Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- README.md +76 -518
- components/__init__.py +14 -0
- components/actor_browser.py +5 -0
- components/log_viewer.py +5 -0
- components/material_preview.py +5 -0
- components/performance_chart.py +5 -0
- components/workflow_form.py +5 -0
- core/__init__.py +7 -0
- core/auth.py +150 -0
- core/config.py +65 -0
- core/connection.py +194 -0
- core/session.py +188 -0
- tabs/__init__.py +14 -0
- tabs/monitoring.py +207 -0
- tabs/scene_control.py +335 -0
- tabs/setup.py +236 -0
- tabs/skills_agents.py +296 -0
- tabs/workflow_builder.py +460 -0
- utils/__init__.py +5 -0
- utils/discovery.py +129 -0
- utils/socket_client.py +186 -0
README.md
CHANGED
|
@@ -1,561 +1,119 @@
|
|
| 1 |
-
# KARiANA.ai MCP Tools for Unreal Engine
|
| 2 |
-
|
| 3 |
-
[](https://www.unrealengine.com/)
|
| 4 |
-
[](https://modelcontextprotocol.io/)
|
| 5 |
-
[](https://www.docker.com/)
|
| 6 |
-
[](https://kerinzeebart.github.io/MCPTest-with-UMCP.it)
|
| 7 |
-
|
| 8 |
-
**Production-ready Docker deployment for 164 Unreal Engine 5.6 MCP tools**
|
| 9 |
-
|
| 10 |
-
Complete testing framework and Docker infrastructure for AI-powered game development automation. Control Unreal Engine programmatically through Model Context Protocol (MCP) with zero-configuration setup.
|
| 11 |
-
|
| 12 |
-
---
|
| 13 |
-
|
| 14 |
-
## 🎯 What is This?
|
| 15 |
-
|
| 16 |
-
A comprehensive, zero-configuration Docker-based system that provides **164 MCP tools** for programmatic control of Unreal Engine 5.6. Perfect for AI-driven game development, automation, and creative workflows.
|
| 17 |
-
|
| 18 |
-
### Key Features
|
| 19 |
-
|
| 20 |
-
- ✅ **164 Tools** - 50+ external MCP actions + 88 plugin features
|
| 21 |
-
- ✅ **Zero Configuration** - Automated setup for Windows, Linux, macOS
|
| 22 |
-
- ✅ **Production Ready** - Security hardened, performance optimized
|
| 23 |
-
- ✅ **UE 5.6 Compatible** - All APIs verified and tested
|
| 24 |
-
- ✅ **Complete Documentation** - 7,000+ lines of comprehensive guides
|
| 25 |
-
- ✅ **GUI Installer** - User-friendly PowerShell installer for Windows
|
| 26 |
-
- ✅ **Auto-Detection** - Plugin automatically detects and configures Docker
|
| 27 |
-
|
| 28 |
---
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
.
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
# 1. Clone repository
|
| 48 |
-
git clone https://github.com/kerinzeebart/MCPTest-with-UMCP.it.git
|
| 49 |
-
cd MCPTest-with-UMCP.it
|
| 50 |
-
|
| 51 |
-
# 2. Install dependencies
|
| 52 |
-
npm install
|
| 53 |
-
|
| 54 |
-
# 3. Auto-detect environment
|
| 55 |
-
python docker-detect.py
|
| 56 |
-
|
| 57 |
-
# 4. Build Docker images
|
| 58 |
-
docker-compose build
|
| 59 |
-
|
| 60 |
-
# 5. Test all tools (requires Unreal Engine running)
|
| 61 |
-
docker-compose up mcp-all-tools
|
| 62 |
-
|
| 63 |
-
# 6. View results
|
| 64 |
-
cat reports/all-164-tools-report.json
|
| 65 |
-
```
|
| 66 |
-
|
| 67 |
-
**Prerequisites:**
|
| 68 |
-
- Unreal Engine 5.6+ with MCPTest.uproject open
|
| 69 |
-
- Remote Control & Python Script plugins enabled
|
| 70 |
-
- Ports 30010 (HTTP), 30020 (WebSocket), 9877 (Socket) available
|
| 71 |
-
|
| 72 |
-
[**Full Setup Guide →**](https://kerinzeebart.github.io/MCPTest-with-UMCP.it/docs/setup-guide)
|
| 73 |
-
|
| 74 |
---
|
| 75 |
|
| 76 |
-
#
|
| 77 |
-
|
| 78 |
-
Visit our [**GitHub Pages Documentation**](https://kerinzeebart.github.io/MCPTest-with-UMCP.it) for comprehensive guides.
|
| 79 |
|
| 80 |
-
|
| 81 |
-
- [**Setup Guide**](https://kerinzeebart.github.io/MCPTest-with-UMCP.it/docs/setup-guide) - Step-by-step installation for all platforms
|
| 82 |
-
- [**Tool Registry**](https://kerinzeebart.github.io/MCPTest-with-UMCP.it/docs/tool-registry) - Complete catalog of all 164 tools
|
| 83 |
-
- [**Quick Start**](#quick-start) - Get running in 5 minutes
|
| 84 |
|
| 85 |
-
|
| 86 |
-
- [**API Reference**](https://kerinzeebart.github.io/MCPTest-with-UMCP.it/docs/api-reference) - Complete API documentation with examples
|
| 87 |
-
- [**Testing Guide**](https://kerinzeebart.github.io/MCPTest-with-UMCP.it/docs/testing) - Comprehensive testing methodology
|
| 88 |
-
- [**UE 5.6 Compatibility**](https://kerinzeebart.github.io/MCPTest-with-UMCP.it/docs/ue56-compatibility) - Compatibility audit and verification
|
| 89 |
|
| 90 |
-
##
|
| 91 |
-
- [**Troubleshooting**](https://kerinzeebart.github.io/MCPTest-with-UMCP.it/docs/troubleshooting) - Problem-solving guide (1000+ lines)
|
| 92 |
-
- [**Implementation Details**](https://kerinzeebart.github.io/MCPTest-with-UMCP.it/docs/implementation) - Technical deep-dive
|
| 93 |
-
- [**Complete Summary**](https://kerinzeebart.github.io/MCPTest-with-UMCP.it/docs/summary) - Master overview document
|
| 94 |
|
| 95 |
-
|
| 96 |
|
| 97 |
-
##
|
| 98 |
|
| 99 |
-
|
| 100 |
-
Via Remote Control API (ports 30010/30020)
|
| 101 |
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
| **Level Management** | 8 | Load, save, create, lighting |
|
| 108 |
-
| **Blueprint Operations** | 4 | Create, compile, add components |
|
| 109 |
-
| **Environment** | 8 | Landscape, foliage, splines, meshes |
|
| 110 |
-
| **Sequencer** | 14 | Cinematics, keyframes, rendering |
|
| 111 |
-
| **System Control** | 8 | Quality, screenshots, stats |
|
| 112 |
|
| 113 |
-
|
| 114 |
|
| 115 |
-
|
| 116 |
-
|
|
|
|
|
|
|
| 117 |
|
| 118 |
-
|
| 119 |
-
|----------|-------|-------------|
|
| 120 |
-
| **Basic Commands** | 11 | Core operations, screenshots, materials |
|
| 121 |
-
| **Actor Management** | 5 | Select, modify, delete actors |
|
| 122 |
-
| **Blueprint Ops** | 13 | Create, edit, compile blueprints |
|
| 123 |
-
| **Blueprint Connections** | 5 | Node connections and validation |
|
| 124 |
-
| **PCG** | 12 | Procedural Content Generation |
|
| 125 |
-
| **Quick Actions** | 23 | Lights, cameras, alignment, organization |
|
| 126 |
-
| **Organization** | 6 | World Outliner organization |
|
| 127 |
|
| 128 |
-
|
| 129 |
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
|
| 132 |
-
##
|
| 133 |
|
|
|
|
| 134 |
```
|
| 135 |
-
|
| 136 |
-
│ User Interface Layer │
|
| 137 |
-
│ GUI Installer | CLI Scripts | Auto-Detect│
|
| 138 |
-
└─────────────────────────────────────────┘
|
| 139 |
-
↓
|
| 140 |
-
┌─────────────────────────────────────────┐
|
| 141 |
-
│ Docker Orchestration (6 Services) │
|
| 142 |
-
│ Test | Comprehensive | Monitor | All │
|
| 143 |
-
└─────────────────────────────────────────┘
|
| 144 |
-
↓
|
| 145 |
-
┌─────────────────────────────────────────┐
|
| 146 |
-
│ MCP Communication Layer │
|
| 147 |
-
│ Remote Control API | Socket Server │
|
| 148 |
-
└─────────────────────────────────────────┘
|
| 149 |
-
↓
|
| 150 |
-
┌─────────────────────────────────────────┐
|
| 151 |
-
│ Unreal Engine 5.6 │
|
| 152 |
-
│ Remote Control Plugin | UMCPitv20 Plugin│
|
| 153 |
-
└─────────────────────────────────────────┘
|
| 154 |
-
```
|
| 155 |
-
|
| 156 |
-
### Docker Infrastructure
|
| 157 |
-
|
| 158 |
-
**Multi-Stage Production Build:**
|
| 159 |
-
- **Builder Stage**: Node.js 22 Alpine with full development dependencies
|
| 160 |
-
- **Production Stage**: Chainguard distroless (CVE-free, 150MB, 70% size reduction)
|
| 161 |
-
- **Security**: Non-root user, minimal attack surface
|
| 162 |
-
- **Performance**: Optimized for <100ms response times
|
| 163 |
-
|
| 164 |
-
**6 Docker Service Configurations:**
|
| 165 |
-
1. `mcp-test` - Quick validation (30 seconds)
|
| 166 |
-
2. `mcp-comprehensive` - Full test suite (5-8 minutes)
|
| 167 |
-
3. `mcp-extended` - Stress testing (10 minutes)
|
| 168 |
-
4. `mcp-monitor` - Live monitoring with auto-restart
|
| 169 |
-
5. `mcp-all-tools` - Complete 164 tool validation
|
| 170 |
-
6. `mcp-dev` - Development mode with hot reload
|
| 171 |
-
|
| 172 |
-
---
|
| 173 |
-
|
| 174 |
-
## 📊 Statistics
|
| 175 |
-
|
| 176 |
-
- **164 Tools** total (50+ external + 88 plugin)
|
| 177 |
-
- **10,000+ lines** of code
|
| 178 |
-
- **7,000+ lines** of documentation
|
| 179 |
-
- **30+ files** created
|
| 180 |
-
- **5 test suites** for validation
|
| 181 |
-
- **3 platforms** supported (Windows/Linux/macOS)
|
| 182 |
-
- **Zero configuration** required
|
| 183 |
-
|
| 184 |
-
---
|
| 185 |
-
|
| 186 |
-
## 🎓 Use Cases
|
| 187 |
-
|
| 188 |
-
### AI-Driven Development
|
| 189 |
-
- Use Claude, ChatGPT, or other AI assistants to control Unreal Engine
|
| 190 |
-
- Natural language to engine commands
|
| 191 |
-
- Automated asset creation and scene building
|
| 192 |
-
|
| 193 |
-
### Automation & Testing
|
| 194 |
-
- Automated level generation
|
| 195 |
-
- Batch asset processing
|
| 196 |
-
- Continuous integration testing
|
| 197 |
-
- Performance benchmarking
|
| 198 |
-
|
| 199 |
-
### Creative Workflows
|
| 200 |
-
- Procedural content generation
|
| 201 |
-
- Rapid prototyping
|
| 202 |
-
- Scene organization
|
| 203 |
-
- Cinematic automation
|
| 204 |
-
|
| 205 |
-
### Remote Development
|
| 206 |
-
- Control Unreal Engine from anywhere
|
| 207 |
-
- Cloud-based workflows
|
| 208 |
-
- Distributed team collaboration
|
| 209 |
-
|
| 210 |
-
---
|
| 211 |
-
|
| 212 |
-
## 🔐 Security Features
|
| 213 |
-
|
| 214 |
-
- ✅ **Distroless Images** - Chainguard base (CVE-free)
|
| 215 |
-
- ✅ **Non-Root Execution** - Enhanced security
|
| 216 |
-
- ✅ **Multi-Stage Builds** - No dev tools in production
|
| 217 |
-
- ✅ **Network Isolation** - Bridge network with no inbound exposure
|
| 218 |
-
- ✅ **Minimal Attack Surface** - 150MB production images
|
| 219 |
-
|
| 220 |
-
---
|
| 221 |
-
|
| 222 |
-
## 🧪 Testing
|
| 223 |
-
|
| 224 |
-
### Quick Test (30s)
|
| 225 |
-
```bash
|
| 226 |
-
docker-compose up mcp-test
|
| 227 |
-
# Tests: Connection, basic tools, health check
|
| 228 |
```
|
| 229 |
|
| 230 |
-
###
|
| 231 |
-
```bash
|
| 232 |
-
npm run test:mcp
|
| 233 |
-
# Tests: All 50+ external MCP actions
|
| 234 |
```
|
| 235 |
-
|
| 236 |
-
### All 164 Tools (10min)
|
| 237 |
-
```bash
|
| 238 |
-
npm run test:all-tools
|
| 239 |
-
# Tests: External + Plugin socket commands
|
| 240 |
```
|
| 241 |
|
| 242 |
-
###
|
| 243 |
-
```bash
|
| 244 |
-
docker-compose up mcp-monitor
|
| 245 |
-
# Continuous health monitoring with auto-restart
|
| 246 |
```
|
| 247 |
-
|
| 248 |
-
### Expected Results
|
| 249 |
-
|
| 250 |
-
**Pass Rate:** 80-95% (depending on project assets and configuration)
|
| 251 |
-
|
| 252 |
-
Tests generate detailed reports in `mcp-test-report.json`:
|
| 253 |
-
```json
|
| 254 |
-
{
|
| 255 |
-
"timestamp": "2025-11-11T...",
|
| 256 |
-
"passed": ["manage_asset.list", "control_actor.spawn", ...],
|
| 257 |
-
"failed": ["animation_physics.play_montage"],
|
| 258 |
-
"summary": {
|
| 259 |
-
"total": 164,
|
| 260 |
-
"passed": 142,
|
| 261 |
-
"failed": 8,
|
| 262 |
-
"skipped": 14,
|
| 263 |
-
"passRate": "86.6%"
|
| 264 |
-
}
|
| 265 |
-
}
|
| 266 |
```
|
| 267 |
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
## 🔧 Configuration
|
| 271 |
-
|
| 272 |
-
### Unreal Engine Setup
|
| 273 |
|
| 274 |
-
**
|
| 275 |
-
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
|
| 279 |
-
|
| 280 |
-
```ini
|
| 281 |
-
[/Script/PythonScriptPlugin.PythonScriptPluginSettings]
|
| 282 |
-
bRemoteExecution=True
|
| 283 |
-
bAllowRemotePythonExecution=True
|
| 284 |
|
| 285 |
-
[/Script/RemoteControl.RemoteControlSettings]
|
| 286 |
-
bAllowRemoteExecutionOfConsoleCommands=True
|
| 287 |
-
bEnableRemoteExecution=True
|
| 288 |
-
bAllowPythonExecution=True
|
| 289 |
```
|
| 290 |
-
|
| 291 |
-
### Claude Desktop Configuration
|
| 292 |
-
|
| 293 |
-
**Location:** `C:\Users\USERNAME\AppData\Roaming\Claude\claude_desktop_config.json`
|
| 294 |
-
|
| 295 |
-
```json
|
| 296 |
-
{
|
| 297 |
-
"mcpServers": {
|
| 298 |
-
"unreal-engine": {
|
| 299 |
-
"command": "node",
|
| 300 |
-
"args": ["path/to/node_modules/unreal-engine-mcp-server/dist/cli.js"],
|
| 301 |
-
"env": {
|
| 302 |
-
"UE_HOST": "127.0.0.1",
|
| 303 |
-
"UE_RC_HTTP_PORT": "30010",
|
| 304 |
-
"UE_RC_WS_PORT": "30020",
|
| 305 |
-
"UE_PROJECT_PATH": "path/to/MCPTest.uproject"
|
| 306 |
-
}
|
| 307 |
-
}
|
| 308 |
-
}
|
| 309 |
-
}
|
| 310 |
```
|
| 311 |
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
---
|
| 315 |
-
|
| 316 |
-
## 💻 Development Commands
|
| 317 |
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
|
| 323 |
-
#
|
| 324 |
-
npm run test:mcp
|
| 325 |
|
| 326 |
-
|
| 327 |
-
npm run test:all-tools
|
| 328 |
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
### Docker Operations
|
| 336 |
-
```bash
|
| 337 |
-
# Build images
|
| 338 |
-
docker-compose build
|
| 339 |
|
| 340 |
-
#
|
| 341 |
-
docker-compose logs -f
|
| 342 |
|
| 343 |
-
|
| 344 |
-
|
| 345 |
|
| 346 |
-
#
|
| 347 |
-
docker-compose down -v --rmi all
|
| 348 |
-
```
|
| 349 |
|
| 350 |
-
|
| 351 |
-
```bash
|
| 352 |
-
# MCP Inspector (web interface)
|
| 353 |
-
npx @modelcontextprotocol/inspector node node_modules/unreal-engine-mcp-server/dist/cli.js
|
| 354 |
-
|
| 355 |
-
# Python health check
|
| 356 |
-
python Plugins/UMCPitv20/Content/Python/test_unreal_system.py
|
| 357 |
-
```
|
| 358 |
-
|
| 359 |
-
---
|
| 360 |
-
|
| 361 |
-
## 🚦 Getting Help
|
| 362 |
-
|
| 363 |
-
### Common Tasks
|
| 364 |
-
- [Install Docker](https://kerinzeebart.github.io/MCPTest-with-UMCP.it/docs/setup-guide#docker-installation)
|
| 365 |
-
- [Configure Unreal Engine](https://kerinzeebart.github.io/MCPTest-with-UMCP.it/docs/setup-guide#unreal-engine-configuration)
|
| 366 |
-
- [Run Tests](https://kerinzeebart.github.io/MCPTest-with-UMCP.it/docs/testing)
|
| 367 |
-
- [Debug Connection Issues](https://kerinzeebart.github.io/MCPTest-with-UMCP.it/docs/troubleshooting#connection-problems)
|
| 368 |
-
|
| 369 |
-
### Common Issues
|
| 370 |
-
|
| 371 |
-
#### UE_NOT_CONNECTED Error
|
| 372 |
-
**Symptoms:** All tools fail with "Unreal Engine is not connected"
|
| 373 |
-
|
| 374 |
-
**Fix:**
|
| 375 |
-
1. Verify Unreal Engine is running with MCPTest.uproject open
|
| 376 |
-
2. Check Remote Control plugin is enabled
|
| 377 |
-
3. Restart Unreal Engine after enabling plugins
|
| 378 |
-
4. Verify ports 30010/30020 not blocked by firewall
|
| 379 |
-
|
| 380 |
-
#### Docker Host Detection Issues
|
| 381 |
-
**Symptoms:** Docker container cannot connect to Unreal Engine
|
| 382 |
-
|
| 383 |
-
**Fix:**
|
| 384 |
-
1. Run auto-detection: `python docker-detect.py`
|
| 385 |
-
2. Verify `.env` file has correct `UE_HOST`:
|
| 386 |
-
- Windows/Mac: `host.docker.internal`
|
| 387 |
-
- Linux: Bridge IP (e.g., `172.17.0.1`)
|
| 388 |
-
3. Test connectivity: `docker-compose up mcp-test`
|
| 389 |
-
|
| 390 |
-
#### Actor Not Found Errors
|
| 391 |
-
**Fix:**
|
| 392 |
-
- Actor names are **case-sensitive**
|
| 393 |
-
- Use exact label from World Outliner
|
| 394 |
-
- Both friendly labels and internal names work
|
| 395 |
-
- Format: `/Game/Maps/Level.Level:PersistentLevel.ActorName`
|
| 396 |
-
|
| 397 |
-
#### Asset Path Errors
|
| 398 |
-
**Fix:**
|
| 399 |
-
- Use `/Game` prefix, NOT `/Content`
|
| 400 |
-
- Correct: `/Game/Materials/MyMat`
|
| 401 |
-
- Wrong: `/Content/Materials/MyMat`
|
| 402 |
-
|
| 403 |
-
[**Full Troubleshooting Guide →**](https://kerinzeebart.github.io/MCPTest-with-UMCP.it/docs/troubleshooting)
|
| 404 |
-
|
| 405 |
-
---
|
| 406 |
-
|
| 407 |
-
## 📁 Project Structure
|
| 408 |
-
|
| 409 |
-
```
|
| 410 |
-
MCPTest-with-UMCP.it/
|
| 411 |
-
├── Dockerfile # Multi-stage production build
|
| 412 |
-
├── docker-compose.yml # 6 service configurations
|
| 413 |
-
├── docker-detect.py # Auto-detection system
|
| 414 |
-
├── setup-docker.sh # Linux/Mac installer
|
| 415 |
-
├── Install-MCPDocker-GUI.ps1 # Windows GUI installer
|
| 416 |
-
│
|
| 417 |
-
├── test-all-164-tools.js # Comprehensive test suite
|
| 418 |
-
├── test-mcp-tools.js # External MCP tools test
|
| 419 |
-
├── test-mcp-connection.js # Connection validation
|
| 420 |
-
│
|
| 421 |
-
├── Plugins/
|
| 422 |
-
│ ├── UMCPitv20/ # Main MCP integration plugin
|
| 423 |
-
│ │ ├── Content/Python/
|
| 424 |
-
│ │ │ ├── docker_integration.py
|
| 425 |
-
│ │ │ ├── unreal_socket_server.py
|
| 426 |
-
│ │ │ └── skills/ # AI workflow skills
|
| 427 |
-
│ │ └── Source/ # C++ plugin source
|
| 428 |
-
│ └── UMCP/ # Original UMCP.it plugin
|
| 429 |
-
│
|
| 430 |
-
├── docs/ # GitHub Pages docs
|
| 431 |
-
├── DOCKER_*.md # Comprehensive guides
|
| 432 |
-
├── API_REFERENCE.md # Complete API docs
|
| 433 |
-
├── _config.yml # Jekyll configuration
|
| 434 |
-
└── index.md # GitHub Pages homepage
|
| 435 |
-
```
|
| 436 |
-
|
| 437 |
-
---
|
| 438 |
-
|
| 439 |
-
## 📊 Performance Metrics
|
| 440 |
-
|
| 441 |
-
### Response Times
|
| 442 |
-
- **External MCP Tools**: <100ms average
|
| 443 |
-
- **Plugin Socket Commands**: <50ms average
|
| 444 |
-
- **Docker Overhead**: <10ms additional latency
|
| 445 |
-
|
| 446 |
-
### Container Metrics
|
| 447 |
-
- **Image Size**: 150MB (production, distroless)
|
| 448 |
-
- **Build Time**: 2-3 minutes (multi-stage)
|
| 449 |
-
- **Memory Usage**: <100MB runtime
|
| 450 |
-
- **CPU Usage**: <5% idle, <20% under load
|
| 451 |
|
| 452 |
---
|
| 453 |
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
### Latest Version: v2.0 (Docker Production Release)
|
| 457 |
-
|
| 458 |
-
#### ✨ Major Features
|
| 459 |
-
- **🐳 Docker Infrastructure**: Zero-configuration deployment with multi-stage builds
|
| 460 |
-
- **🔒 Security Hardening**: Chainguard distroless images, CVE-free, non-root user
|
| 461 |
-
- **📦 Complete Tool Coverage**: All 164 tools (50+ external + 88 plugin)
|
| 462 |
-
- **🚀 Automated Installers**: GUI for Windows, CLI for Linux/Mac
|
| 463 |
-
- **📊 Comprehensive Testing**: 5 test scenarios with detailed reporting
|
| 464 |
-
- **📚 GitHub Pages Docs**: Professional documentation site
|
| 465 |
-
- **🔍 Auto-Detection**: Platform-specific environment configuration
|
| 466 |
-
|
| 467 |
-
#### 🔧 Critical Fixes (October 2025)
|
| 468 |
-
- **Fixed**: actor_modify tool now works with both friendly labels AND internal object names
|
| 469 |
-
- **Fixed**: Claude Desktop spinning wheel (responses return in <1 second)
|
| 470 |
-
- **Fixed**: Socket server newline delimiter for immediate response handling
|
| 471 |
-
|
| 472 |
-
#### 🎯 Skills Framework Integration
|
| 473 |
-
- **NEW**: Higher-level AI workflows with expert prompts
|
| 474 |
-
- **unreal-organizer**: AI-powered World Outliner organization
|
| 475 |
-
- **discovery-first**: Dynamic project structure discovery (no hardcoded paths)
|
| 476 |
-
|
| 477 |
-
---
|
| 478 |
-
|
| 479 |
-
## 🏢 About KARiANA.ai
|
| 480 |
-
|
| 481 |
-
**KARiANA.ai** is revolutionizing game development through AI-powered automation and intelligent workflows. This MCP Tools implementation demonstrates our commitment to making Unreal Engine accessible to AI assistants and automation systems.
|
| 482 |
-
|
| 483 |
-
### Our Vision
|
| 484 |
-
Transform game development workflows by enabling natural language control of complex engine operations, making advanced features accessible to developers of all skill levels.
|
| 485 |
-
|
| 486 |
-
### Key Projects
|
| 487 |
-
- **MCP Tools for Unreal Engine**: 164 production-ready tools for AI-driven development
|
| 488 |
-
- **Docker Infrastructure**: Zero-configuration deployment for enterprise environments
|
| 489 |
-
- **Skills Framework**: Higher-level AI workflows for complex game development tasks
|
| 490 |
-
|
| 491 |
-
### Connect With Us
|
| 492 |
-
- **Website**: [KARiANA.ai](https://kariana.ai) (coming soon)
|
| 493 |
-
- **Documentation**: [GitHub Pages](https://kerinzeebart.github.io/MCPTest-with-UMCP.it)
|
| 494 |
-
- **Repository**: [GitHub](https://github.com/kerinzeebart/MCPTest-with-UMCP.it)
|
| 495 |
-
|
| 496 |
-
---
|
| 497 |
-
|
| 498 |
-
## 📚 Additional Resources
|
| 499 |
-
|
| 500 |
-
### Official Documentation
|
| 501 |
-
- [Model Context Protocol](https://modelcontextprotocol.io/)
|
| 502 |
-
- [Unreal Engine Remote Control API](https://docs.unrealengine.com/5.6/en-US/remote-control-api-in-unreal-engine/)
|
| 503 |
-
- [Unreal Engine Python API](https://docs.unrealengine.com/5.6/en-US/scripting-the-unreal-editor-using-python/)
|
| 504 |
-
|
| 505 |
-
### Project Documentation
|
| 506 |
-
- [Docker Setup Guide](DOCKER_SETUP_GUIDE.md) - Complete installation instructions
|
| 507 |
-
- [Testing Guide](DOCKER_TESTING.md) - Test scenarios and KPIs
|
| 508 |
-
- [Troubleshooting](DOCKER_TROUBLESHOOTING.md) - 50+ common issues
|
| 509 |
-
- [API Reference](API_REFERENCE.md) - Complete API for all 164 tools
|
| 510 |
-
- [Implementation Details](DOCKER_IMPLEMENTATION.md) - Architecture & design
|
| 511 |
-
- [UE 5.6 Compatibility](UE5.6_API_COMPATIBILITY_AUDIT.md) - Compatibility audit
|
| 512 |
-
- [Complete Summary](DOCKER_COMPLETE_IMPLEMENTATION_SUMMARY.md) - Master overview
|
| 513 |
-
|
| 514 |
-
---
|
| 515 |
-
|
| 516 |
-
## ✅ Success Criteria
|
| 517 |
-
|
| 518 |
-
- [x] **164 tools implemented** (50+ external + 88 plugin)
|
| 519 |
-
- [x] **Docker infrastructure complete** (6 service configurations)
|
| 520 |
-
- [x] **Zero-configuration setup** (automated installers)
|
| 521 |
-
- [x] **Security hardened** (CVE-free distroless images)
|
| 522 |
-
- [x] **80%+ test pass rate** (comprehensive automated testing)
|
| 523 |
-
- [x] **Complete documentation** (7,000+ lines, GitHub Pages)
|
| 524 |
-
- [x] **UE 5.6 compatible** (all APIs verified)
|
| 525 |
-
- [x] **Production ready** (<100ms response times)
|
| 526 |
-
|
| 527 |
-
---
|
| 528 |
-
|
| 529 |
-
## 📝 License
|
| 530 |
-
|
| 531 |
-
This project is provided as-is for development and testing purposes. See individual component licenses:
|
| 532 |
-
- Unreal Engine MCP Server: [unreal-engine-mcp-server](https://github.com/kenstclair/unreal-engine-mcp-server)
|
| 533 |
-
- UMCPitv20 Plugin: Custom implementation for KARiANA.ai
|
| 534 |
-
- Docker Infrastructure: KARiANA.ai (MIT-style)
|
| 535 |
-
|
| 536 |
-
---
|
| 537 |
-
|
| 538 |
-
## 🤝 Contributing
|
| 539 |
-
|
| 540 |
-
We welcome contributions! Please see our documentation for:
|
| 541 |
-
- [Development Guidelines](DOCKER_IMPLEMENTATION.md)
|
| 542 |
-
- [Testing Requirements](DOCKER_TESTING.md)
|
| 543 |
-
- [API Standards](API_REFERENCE.md)
|
| 544 |
-
|
| 545 |
-
---
|
| 546 |
-
|
| 547 |
-
## 🎉 Status: Production Ready
|
| 548 |
-
|
| 549 |
-
All systems tested, documented, and ready for immediate use.
|
| 550 |
-
|
| 551 |
-
[**Get Started Now →**](https://kerinzeebart.github.io/MCPTest-with-UMCP.it/docs/setup-guide)
|
| 552 |
-
|
| 553 |
-
---
|
| 554 |
-
|
| 555 |
-
<div align="center">
|
| 556 |
-
|
| 557 |
-
**Made with ❤️ by KARiANA.ai**
|
| 558 |
-
|
| 559 |
-
*Empowering AI-driven game development*
|
| 560 |
-
|
| 561 |
-
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Kariana UMCP
|
| 3 |
+
emoji: 🎮
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 5.9.0
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
+
short_description: AI-Powered Virtual Production Control for Unreal Engine
|
| 12 |
+
tags:
|
| 13 |
+
- mcp
|
| 14 |
+
- mcp-in-action-track-xx
|
| 15 |
+
- unreal-engine
|
| 16 |
+
- virtual-production
|
| 17 |
+
- gradio
|
| 18 |
+
- claude
|
| 19 |
+
- hackathon
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
---
|
| 21 |
|
| 22 |
+
# Kariana UMCP - AI-Powered Virtual Production Control
|
|
|
|
|
|
|
| 23 |
|
| 24 |
+
**MCP 1st Birthday Hackathon Submission | Track: MCP in Action | Category: Multimodal + Productivity**
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
+
> *"What if you could talk to Unreal Engine like you talk to a creative partner?"*
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
+
## The Problem
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
+
Virtual production sets are complex. Lighting directors, environment artists, and cinematographers spend hours clicking through menus, adjusting parameters, and coordinating changes. The gap between creative intent and technical execution slows down production.
|
| 31 |
|
| 32 |
+
## The Solution: Kariana UMCP
|
| 33 |
|
| 34 |
+
A unified Gradio dashboard that connects Claude AI to Unreal Engine 5.6 via MCP, enabling:
|
|
|
|
| 35 |
|
| 36 |
+
- **Natural language scene control**: "Add dramatic backlighting to the hero character"
|
| 37 |
+
- **Real-time monitoring**: Live FPS, logs, and connection status
|
| 38 |
+
- **Visual scene manipulation**: Actor browser, transforms, materials
|
| 39 |
+
- **Workflow automation**: Create reusable multi-step workflows
|
| 40 |
+
- **Claude Skills integration**: 8 specialized AI agents for complex tasks
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
+
### Simple PIN-Based Authentication
|
| 43 |
|
| 44 |
+
No complex tokens. When Unreal starts:
|
| 45 |
+
1. A 4-digit PIN appears in the editor log
|
| 46 |
+
2. Enter it in the Gradio UI
|
| 47 |
+
3. You're connected!
|
| 48 |
|
| 49 |
+
## Features
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
+
### 5-Tab Dashboard
|
| 52 |
|
| 53 |
+
| Tab | Features |
|
| 54 |
+
|-----|----------|
|
| 55 |
+
| **Monitor** | Connection status, FPS metrics, real-time logs |
|
| 56 |
+
| **Scene** | Actor browser, transforms, screenshots |
|
| 57 |
+
| **Skills** | 8 Claude agents, skill execution |
|
| 58 |
+
| **Workflow** | Form-based automation builder |
|
| 59 |
+
| **Setup** | PIN auth, instance discovery |
|
| 60 |
|
| 61 |
+
## Virtual Production Use Cases
|
| 62 |
|
| 63 |
+
### For Lighting Directors
|
| 64 |
```
|
| 65 |
+
"Create a 3-point lighting setup with key at 45 degrees"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
```
|
| 67 |
|
| 68 |
+
### For Environment Artists
|
|
|
|
|
|
|
|
|
|
| 69 |
```
|
| 70 |
+
"Scatter 100 trees in the meadow area with natural variation"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
```
|
| 72 |
|
| 73 |
+
### For Cinematographers
|
|
|
|
|
|
|
|
|
|
| 74 |
```
|
| 75 |
+
"Capture a 360-degree turntable of the hero asset"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
```
|
| 77 |
|
| 78 |
+
## How to Connect
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
+
1. **Install KarianaUMCP plugin** in your Unreal Engine 5.6+ project
|
| 81 |
+
2. **Open your project** - the plugin auto-starts and displays a 4-digit PIN
|
| 82 |
+
3. **Enter PIN** in the Setup tab of this Space
|
| 83 |
+
4. **Start creating!**
|
| 84 |
|
| 85 |
+
## Technical Architecture
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
```
|
| 88 |
+
Gradio UI (HF Spaces) → Socket Server (Port 9877) → Unreal Engine 5.6
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
```
|
| 90 |
|
| 91 |
+
## Built With
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
+
- **Gradio 5.0+** - Beautiful UI
|
| 94 |
+
- **FastMCP 2.13+** - MCP server framework
|
| 95 |
+
- **Unreal Engine 5.6** - Real-time engine
|
| 96 |
+
- **Claude AI** - Natural language understanding
|
| 97 |
|
| 98 |
+
## Download Plugin
|
|
|
|
| 99 |
|
| 100 |
+
Get the KarianaUMCP plugin for your Unreal Engine project:
|
|
|
|
| 101 |
|
| 102 |
+
| Platform | Download |
|
| 103 |
+
|----------|----------|
|
| 104 |
+
| **Windows** | [KarianaUMCP-v1.0.0-windows.zip](https://github.com/kerinzeebart/MCPTest-with-UMCP.it/releases/download/v1.0.0/KarianaUMCP-v1.0.0-windows.zip) |
|
| 105 |
+
| **macOS** | [KarianaUMCP-v1.0.0-macos.zip](https://github.com/kerinzeebart/MCPTest-with-UMCP.it/releases/download/v1.0.0/KarianaUMCP-v1.0.0-macos.zip) |
|
| 106 |
+
| **Linux** | [KarianaUMCP-v1.0.0-linux.zip](https://github.com/kerinzeebart/MCPTest-with-UMCP.it/releases/download/v1.0.0/KarianaUMCP-v1.0.0-linux.zip) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
+
## Links
|
|
|
|
| 109 |
|
| 110 |
+
- **GitHub**: [Kariana-UMCP](https://github.com/kerinzeebart/MCPTest-with-UMCP.it)
|
| 111 |
+
- **Hackathon**: [MCP 1st Birthday](https://huggingface.co/MCP-1st-Birthday)
|
| 112 |
|
| 113 |
+
## Team
|
|
|
|
|
|
|
| 114 |
|
| 115 |
+
- **barlowski** - Developer
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
---
|
| 118 |
|
| 119 |
+
*Happy Birthday, MCP! Here's to AI-powered creativity.* 🎂
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
components/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Kariana Unified UI - Components Module
|
| 2 |
+
from .actor_browser import ActorBrowser
|
| 3 |
+
from .material_preview import MaterialPreview
|
| 4 |
+
from .log_viewer import LogViewer
|
| 5 |
+
from .performance_chart import PerformanceChart
|
| 6 |
+
from .workflow_form import WorkflowForm
|
| 7 |
+
|
| 8 |
+
__all__ = [
|
| 9 |
+
'ActorBrowser',
|
| 10 |
+
'MaterialPreview',
|
| 11 |
+
'LogViewer',
|
| 12 |
+
'PerformanceChart',
|
| 13 |
+
'WorkflowForm'
|
| 14 |
+
]
|
components/actor_browser.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Actor Browser Component - Placeholder for future expansion"""
|
| 2 |
+
|
| 3 |
+
class ActorBrowser:
|
| 4 |
+
"""Actor browser component for scene tree visualization"""
|
| 5 |
+
pass
|
components/log_viewer.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Log Viewer Component - Placeholder for future expansion"""
|
| 2 |
+
|
| 3 |
+
class LogViewer:
|
| 4 |
+
"""Real-time log viewer component"""
|
| 5 |
+
pass
|
components/material_preview.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Material Preview Component - Placeholder for future expansion"""
|
| 2 |
+
|
| 3 |
+
class MaterialPreview:
|
| 4 |
+
"""Material preview component for visual material editing"""
|
| 5 |
+
pass
|
components/performance_chart.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Performance Chart Component - Placeholder for future expansion"""
|
| 2 |
+
|
| 3 |
+
class PerformanceChart:
|
| 4 |
+
"""Performance metrics chart component"""
|
| 5 |
+
pass
|
components/workflow_form.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Workflow Form Component - Placeholder for future expansion"""
|
| 2 |
+
|
| 3 |
+
class WorkflowForm:
|
| 4 |
+
"""Form-based workflow builder component"""
|
| 5 |
+
pass
|
core/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Kariana Unified UI - Core Module
|
| 2 |
+
from .config import Config
|
| 3 |
+
from .auth import PINAuthenticator
|
| 4 |
+
from .connection import UnrealConnection
|
| 5 |
+
from .session import SessionManager
|
| 6 |
+
|
| 7 |
+
__all__ = ['Config', 'PINAuthenticator', 'UnrealConnection', 'SessionManager']
|
core/auth.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Kariana Unified UI - PIN Authentication
|
| 3 |
+
Simple 4-digit PIN-based authentication with Unreal instances
|
| 4 |
+
"""
|
| 5 |
+
import asyncio
|
| 6 |
+
import json
|
| 7 |
+
import logging
|
| 8 |
+
from typing import Optional, Tuple
|
| 9 |
+
|
| 10 |
+
from ..utils.discovery import InstanceDiscovery, UnrealInstance
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class PINAuthenticator:
|
| 16 |
+
"""Handles PIN-based authentication with Unreal instances"""
|
| 17 |
+
|
| 18 |
+
def __init__(
|
| 19 |
+
self,
|
| 20 |
+
host: str = "localhost",
|
| 21 |
+
port_range: tuple = (9877, 9887),
|
| 22 |
+
timeout: float = 5.0
|
| 23 |
+
):
|
| 24 |
+
self.host = host
|
| 25 |
+
self.port_range = port_range
|
| 26 |
+
self.timeout = timeout
|
| 27 |
+
self._discovery = InstanceDiscovery(host, port_range, timeout=1.0)
|
| 28 |
+
self._authenticated_instance: Optional[UnrealInstance] = None
|
| 29 |
+
self._session_token: Optional[str] = None
|
| 30 |
+
|
| 31 |
+
@property
|
| 32 |
+
def is_authenticated(self) -> bool:
|
| 33 |
+
return self._authenticated_instance is not None
|
| 34 |
+
|
| 35 |
+
@property
|
| 36 |
+
def current_instance(self) -> Optional[UnrealInstance]:
|
| 37 |
+
return self._authenticated_instance
|
| 38 |
+
|
| 39 |
+
async def validate_pin(self, pin: str, instance: UnrealInstance) -> Tuple[bool, Optional[str]]:
|
| 40 |
+
"""Validate PIN against a specific instance"""
|
| 41 |
+
try:
|
| 42 |
+
reader, writer = await asyncio.wait_for(
|
| 43 |
+
asyncio.open_connection(instance.host, instance.port),
|
| 44 |
+
timeout=self.timeout
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
# Send validate_pin command
|
| 48 |
+
command = json.dumps({
|
| 49 |
+
"type": "validate_pin",
|
| 50 |
+
"pin": pin
|
| 51 |
+
}) + "\n"
|
| 52 |
+
writer.write(command.encode())
|
| 53 |
+
await writer.drain()
|
| 54 |
+
|
| 55 |
+
# Read response
|
| 56 |
+
response_data = await asyncio.wait_for(
|
| 57 |
+
reader.readline(),
|
| 58 |
+
timeout=self.timeout
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
writer.close()
|
| 62 |
+
await writer.wait_closed()
|
| 63 |
+
|
| 64 |
+
if response_data:
|
| 65 |
+
response = json.loads(response_data.decode().strip())
|
| 66 |
+
if response.get("success"):
|
| 67 |
+
return True, response.get("session_token")
|
| 68 |
+
else:
|
| 69 |
+
logger.warning(f"PIN validation failed: {response.get('error')}")
|
| 70 |
+
return False, None
|
| 71 |
+
|
| 72 |
+
except asyncio.TimeoutError:
|
| 73 |
+
logger.warning(f"Timeout validating PIN on {instance.host}:{instance.port}")
|
| 74 |
+
except ConnectionRefusedError:
|
| 75 |
+
logger.warning(f"Connection refused on {instance.host}:{instance.port}")
|
| 76 |
+
except Exception as e:
|
| 77 |
+
logger.error(f"PIN validation error: {e}")
|
| 78 |
+
|
| 79 |
+
return False, None
|
| 80 |
+
|
| 81 |
+
async def authenticate_with_pin(self, pin: str) -> Tuple[bool, Optional[UnrealInstance]]:
|
| 82 |
+
"""
|
| 83 |
+
Auto-discover instances and validate PIN.
|
| 84 |
+
Returns (success, authenticated_instance)
|
| 85 |
+
"""
|
| 86 |
+
# Validate PIN format
|
| 87 |
+
if not pin or len(pin) != 4 or not pin.isdigit():
|
| 88 |
+
logger.warning("Invalid PIN format (must be 4 digits)")
|
| 89 |
+
return False, None
|
| 90 |
+
|
| 91 |
+
# Discover available instances
|
| 92 |
+
instances = await self._discovery.discover_instances()
|
| 93 |
+
|
| 94 |
+
if not instances:
|
| 95 |
+
logger.warning("No Unreal instances found")
|
| 96 |
+
return False, None
|
| 97 |
+
|
| 98 |
+
# Try each instance
|
| 99 |
+
for instance in instances:
|
| 100 |
+
success, session_token = await self.validate_pin(pin, instance)
|
| 101 |
+
if success:
|
| 102 |
+
instance.authenticated = True
|
| 103 |
+
self._authenticated_instance = instance
|
| 104 |
+
self._session_token = session_token
|
| 105 |
+
logger.info(f"Authenticated with instance on port {instance.port}")
|
| 106 |
+
return True, instance
|
| 107 |
+
|
| 108 |
+
logger.warning("PIN did not match any instance")
|
| 109 |
+
return False, None
|
| 110 |
+
|
| 111 |
+
def authenticate_with_pin_sync(self, pin: str) -> Tuple[bool, Optional[UnrealInstance]]:
|
| 112 |
+
"""Synchronous wrapper for authenticate_with_pin"""
|
| 113 |
+
try:
|
| 114 |
+
loop = asyncio.get_running_loop()
|
| 115 |
+
import concurrent.futures
|
| 116 |
+
with concurrent.futures.ThreadPoolExecutor() as pool:
|
| 117 |
+
future = pool.submit(asyncio.run, self.authenticate_with_pin(pin))
|
| 118 |
+
return future.result(timeout=self.timeout * 10 + 1)
|
| 119 |
+
except RuntimeError:
|
| 120 |
+
return asyncio.run(self.authenticate_with_pin(pin))
|
| 121 |
+
|
| 122 |
+
def disconnect(self):
|
| 123 |
+
"""Clear authentication state"""
|
| 124 |
+
self._authenticated_instance = None
|
| 125 |
+
self._session_token = None
|
| 126 |
+
|
| 127 |
+
def get_session_info(self) -> Optional[dict]:
|
| 128 |
+
"""Get current session information"""
|
| 129 |
+
if not self.is_authenticated:
|
| 130 |
+
return None
|
| 131 |
+
|
| 132 |
+
return {
|
| 133 |
+
"instance": self._authenticated_instance.to_dict(),
|
| 134 |
+
"session_token": self._session_token,
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
# Global authenticator instance
|
| 139 |
+
_authenticator: Optional[PINAuthenticator] = None
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def get_authenticator(
|
| 143 |
+
host: str = "localhost",
|
| 144 |
+
port_range: tuple = (9877, 9887)
|
| 145 |
+
) -> PINAuthenticator:
|
| 146 |
+
"""Get or create global authenticator instance"""
|
| 147 |
+
global _authenticator
|
| 148 |
+
if _authenticator is None:
|
| 149 |
+
_authenticator = PINAuthenticator(host, port_range)
|
| 150 |
+
return _authenticator
|
core/config.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Kariana Unified UI - Configuration Management
|
| 3 |
+
"""
|
| 4 |
+
import os
|
| 5 |
+
from dataclasses import dataclass, field
|
| 6 |
+
from typing import Optional
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@dataclass
|
| 10 |
+
class Config:
|
| 11 |
+
"""Configuration for Kariana Unified UI"""
|
| 12 |
+
|
| 13 |
+
# Server settings
|
| 14 |
+
gradio_port: int = 7860
|
| 15 |
+
gradio_host: str = "0.0.0.0"
|
| 16 |
+
|
| 17 |
+
# Unreal connection settings
|
| 18 |
+
unreal_port_range: tuple = (9877, 9887)
|
| 19 |
+
unreal_host: str = "localhost"
|
| 20 |
+
connection_timeout: float = 5.0
|
| 21 |
+
|
| 22 |
+
# Authentication
|
| 23 |
+
pin_length: int = 4
|
| 24 |
+
auth_enabled: bool = True
|
| 25 |
+
|
| 26 |
+
# Deployment mode
|
| 27 |
+
is_huggingface: bool = field(default_factory=lambda: os.getenv("SPACE_ID") is not None)
|
| 28 |
+
remote_mode: bool = False
|
| 29 |
+
|
| 30 |
+
# Logging
|
| 31 |
+
log_level: str = "INFO"
|
| 32 |
+
|
| 33 |
+
# Tunnel settings (for remote access)
|
| 34 |
+
ngrok_auth_token: Optional[str] = field(default_factory=lambda: os.getenv("NGROK_AUTH_TOKEN"))
|
| 35 |
+
tunnel_enabled: bool = False
|
| 36 |
+
|
| 37 |
+
@classmethod
|
| 38 |
+
def from_env(cls) -> "Config":
|
| 39 |
+
"""Create config from environment variables"""
|
| 40 |
+
return cls(
|
| 41 |
+
gradio_port=int(os.getenv("GRADIO_PORT", "7860")),
|
| 42 |
+
gradio_host=os.getenv("GRADIO_HOST", "0.0.0.0"),
|
| 43 |
+
unreal_host=os.getenv("UNREAL_HOST", "localhost"),
|
| 44 |
+
connection_timeout=float(os.getenv("CONNECTION_TIMEOUT", "5.0")),
|
| 45 |
+
auth_enabled=os.getenv("AUTH_ENABLED", "true").lower() == "true",
|
| 46 |
+
remote_mode=os.getenv("REMOTE_MODE", "false").lower() == "true",
|
| 47 |
+
log_level=os.getenv("LOG_LEVEL", "INFO"),
|
| 48 |
+
tunnel_enabled=os.getenv("TUNNEL_ENABLED", "false").lower() == "true",
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
def to_dict(self) -> dict:
|
| 52 |
+
"""Convert config to dictionary"""
|
| 53 |
+
return {
|
| 54 |
+
"gradio_port": self.gradio_port,
|
| 55 |
+
"gradio_host": self.gradio_host,
|
| 56 |
+
"unreal_port_range": self.unreal_port_range,
|
| 57 |
+
"unreal_host": self.unreal_host,
|
| 58 |
+
"connection_timeout": self.connection_timeout,
|
| 59 |
+
"pin_length": self.pin_length,
|
| 60 |
+
"auth_enabled": self.auth_enabled,
|
| 61 |
+
"is_huggingface": self.is_huggingface,
|
| 62 |
+
"remote_mode": self.remote_mode,
|
| 63 |
+
"log_level": self.log_level,
|
| 64 |
+
"tunnel_enabled": self.tunnel_enabled,
|
| 65 |
+
}
|
core/connection.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Kariana Unified UI - Unreal Connection Manager
|
| 3 |
+
Manages connection state and provides command interface
|
| 4 |
+
"""
|
| 5 |
+
import asyncio
|
| 6 |
+
import logging
|
| 7 |
+
from typing import Any, Dict, List, Optional, Callable
|
| 8 |
+
|
| 9 |
+
from ..utils.socket_client import SocketClient, SyncSocketClient
|
| 10 |
+
from ..utils.discovery import UnrealInstance
|
| 11 |
+
from .auth import PINAuthenticator, get_authenticator
|
| 12 |
+
|
| 13 |
+
logger = logging.getLogger(__name__)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class UnrealConnection:
|
| 17 |
+
"""
|
| 18 |
+
Manages connection to a KarianaUMCP instance.
|
| 19 |
+
Provides high-level command interface.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
def __init__(self, instance: Optional[UnrealInstance] = None):
|
| 23 |
+
self._instance = instance
|
| 24 |
+
self._client: Optional[SyncSocketClient] = None
|
| 25 |
+
self._authenticator = get_authenticator()
|
| 26 |
+
|
| 27 |
+
@property
|
| 28 |
+
def is_connected(self) -> bool:
|
| 29 |
+
return self._instance is not None and self._instance.authenticated
|
| 30 |
+
|
| 31 |
+
@property
|
| 32 |
+
def instance(self) -> Optional[UnrealInstance]:
|
| 33 |
+
return self._instance
|
| 34 |
+
|
| 35 |
+
def connect_with_pin(self, pin: str) -> bool:
|
| 36 |
+
"""Connect using PIN authentication"""
|
| 37 |
+
success, instance = self._authenticator.authenticate_with_pin_sync(pin)
|
| 38 |
+
if success and instance:
|
| 39 |
+
self._instance = instance
|
| 40 |
+
self._client = SyncSocketClient(instance.host, instance.port)
|
| 41 |
+
return True
|
| 42 |
+
return False
|
| 43 |
+
|
| 44 |
+
def disconnect(self):
|
| 45 |
+
"""Disconnect from instance"""
|
| 46 |
+
if self._client:
|
| 47 |
+
self._client.disconnect()
|
| 48 |
+
self._instance = None
|
| 49 |
+
self._authenticator.disconnect()
|
| 50 |
+
|
| 51 |
+
def send_command(self, command_type: str, **params) -> Optional[Dict[str, Any]]:
|
| 52 |
+
"""Send command to Unreal instance"""
|
| 53 |
+
if not self.is_connected:
|
| 54 |
+
logger.warning("Not connected to any instance")
|
| 55 |
+
return None
|
| 56 |
+
|
| 57 |
+
if not self._client:
|
| 58 |
+
self._client = SyncSocketClient(self._instance.host, self._instance.port)
|
| 59 |
+
|
| 60 |
+
command = {"type": command_type, **params}
|
| 61 |
+
return self._client.send_command(command)
|
| 62 |
+
|
| 63 |
+
# === High-Level Command Methods ===
|
| 64 |
+
|
| 65 |
+
def ping(self) -> bool:
|
| 66 |
+
"""Test connection"""
|
| 67 |
+
response = self.send_command("ping")
|
| 68 |
+
return response is not None and response.get("status") == "ok"
|
| 69 |
+
|
| 70 |
+
def get_server_info(self) -> Optional[Dict]:
|
| 71 |
+
"""Get server information"""
|
| 72 |
+
return self.send_command("get_server_info")
|
| 73 |
+
|
| 74 |
+
def list_actors(self) -> Optional[List[Dict]]:
|
| 75 |
+
"""Get list of actors in current level"""
|
| 76 |
+
response = self.send_command("list_actors")
|
| 77 |
+
if response and response.get("success"):
|
| 78 |
+
return response.get("actors", [])
|
| 79 |
+
return None
|
| 80 |
+
|
| 81 |
+
def spawn_actor(
|
| 82 |
+
self,
|
| 83 |
+
actor_type: str,
|
| 84 |
+
name: str,
|
| 85 |
+
location: List[float] = None,
|
| 86 |
+
rotation: List[float] = None,
|
| 87 |
+
scale: List[float] = None
|
| 88 |
+
) -> Optional[Dict]:
|
| 89 |
+
"""Spawn an actor in the level"""
|
| 90 |
+
params = {
|
| 91 |
+
"actor_type": actor_type,
|
| 92 |
+
"name": name,
|
| 93 |
+
}
|
| 94 |
+
if location:
|
| 95 |
+
params["location"] = location
|
| 96 |
+
if rotation:
|
| 97 |
+
params["rotation"] = rotation
|
| 98 |
+
if scale:
|
| 99 |
+
params["scale"] = scale
|
| 100 |
+
|
| 101 |
+
return self.send_command("spawn_actor", **params)
|
| 102 |
+
|
| 103 |
+
def delete_actor(self, actor_name: str) -> Optional[Dict]:
|
| 104 |
+
"""Delete an actor by name"""
|
| 105 |
+
return self.send_command("delete_actor", actor_name=actor_name)
|
| 106 |
+
|
| 107 |
+
def get_actor_transform(self, actor_name: str) -> Optional[Dict]:
|
| 108 |
+
"""Get actor transform (location, rotation, scale)"""
|
| 109 |
+
return self.send_command("get_actor_transform", actor_name=actor_name)
|
| 110 |
+
|
| 111 |
+
def set_actor_transform(
|
| 112 |
+
self,
|
| 113 |
+
actor_name: str,
|
| 114 |
+
location: List[float] = None,
|
| 115 |
+
rotation: List[float] = None,
|
| 116 |
+
scale: List[float] = None
|
| 117 |
+
) -> Optional[Dict]:
|
| 118 |
+
"""Set actor transform"""
|
| 119 |
+
params = {"actor_name": actor_name}
|
| 120 |
+
if location:
|
| 121 |
+
params["location"] = location
|
| 122 |
+
if rotation:
|
| 123 |
+
params["rotation"] = rotation
|
| 124 |
+
if scale:
|
| 125 |
+
params["scale"] = scale
|
| 126 |
+
|
| 127 |
+
return self.send_command("set_actor_transform", **params)
|
| 128 |
+
|
| 129 |
+
def capture_screenshot(self, filename: str = None) -> Optional[Dict]:
|
| 130 |
+
"""Capture viewport screenshot"""
|
| 131 |
+
params = {}
|
| 132 |
+
if filename:
|
| 133 |
+
params["filename"] = filename
|
| 134 |
+
return self.send_command("capture_screenshot", **params)
|
| 135 |
+
|
| 136 |
+
def execute_python(self, code: str) -> Optional[Dict]:
|
| 137 |
+
"""Execute Python code in Unreal"""
|
| 138 |
+
return self.send_command("execute_python", code=code)
|
| 139 |
+
|
| 140 |
+
def console_command(self, command: str) -> Optional[Dict]:
|
| 141 |
+
"""Execute Unreal console command"""
|
| 142 |
+
return self.send_command("console_command", command=command)
|
| 143 |
+
|
| 144 |
+
def get_logs(self, limit: int = 100, category: str = None) -> Optional[List[str]]:
|
| 145 |
+
"""Get Unreal Engine logs"""
|
| 146 |
+
params = {"limit": limit}
|
| 147 |
+
if category:
|
| 148 |
+
params["category"] = category
|
| 149 |
+
|
| 150 |
+
response = self.send_command("get_ue_logs", **params)
|
| 151 |
+
if response and response.get("success"):
|
| 152 |
+
return response.get("logs", [])
|
| 153 |
+
return None
|
| 154 |
+
|
| 155 |
+
def get_performance_metrics(self) -> Optional[Dict]:
|
| 156 |
+
"""Get performance metrics (FPS, frame time, etc.)"""
|
| 157 |
+
return self.send_command("get_performance_metrics")
|
| 158 |
+
|
| 159 |
+
def list_skills(self) -> Optional[List[Dict]]:
|
| 160 |
+
"""List available skills"""
|
| 161 |
+
response = self.send_command("list_skills")
|
| 162 |
+
if response and response.get("success"):
|
| 163 |
+
return response.get("skills", [])
|
| 164 |
+
return None
|
| 165 |
+
|
| 166 |
+
def execute_skill(self, skill_name: str, params: Dict = None) -> Optional[Dict]:
|
| 167 |
+
"""Execute a skill"""
|
| 168 |
+
command_params = {"skill_name": skill_name}
|
| 169 |
+
if params:
|
| 170 |
+
command_params["params"] = params
|
| 171 |
+
return self.send_command("execute_skill", **command_params)
|
| 172 |
+
|
| 173 |
+
def get_actor_tree(self) -> Optional[Dict]:
|
| 174 |
+
"""Get hierarchical actor tree (World Outliner structure)"""
|
| 175 |
+
return self.send_command("get_actor_tree")
|
| 176 |
+
|
| 177 |
+
def get_actor_materials(self, actor_name: str) -> Optional[List[Dict]]:
|
| 178 |
+
"""Get materials assigned to an actor"""
|
| 179 |
+
response = self.send_command("get_actor_materials", actor_name=actor_name)
|
| 180 |
+
if response and response.get("success"):
|
| 181 |
+
return response.get("materials", [])
|
| 182 |
+
return None
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
# Global connection instance
|
| 186 |
+
_connection: Optional[UnrealConnection] = None
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def get_connection() -> UnrealConnection:
|
| 190 |
+
"""Get or create global connection instance"""
|
| 191 |
+
global _connection
|
| 192 |
+
if _connection is None:
|
| 193 |
+
_connection = UnrealConnection()
|
| 194 |
+
return _connection
|
core/session.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Kariana Unified UI - Session Management
|
| 3 |
+
Manages user sessions and persists state
|
| 4 |
+
"""
|
| 5 |
+
import json
|
| 6 |
+
import logging
|
| 7 |
+
import os
|
| 8 |
+
import tempfile
|
| 9 |
+
from dataclasses import dataclass, field, asdict
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
from typing import Dict, List, Optional
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
logger = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@dataclass
|
| 18 |
+
class Session:
|
| 19 |
+
"""User session data"""
|
| 20 |
+
session_id: str
|
| 21 |
+
created_at: str
|
| 22 |
+
last_activity: str
|
| 23 |
+
connected_instance_port: Optional[int] = None
|
| 24 |
+
connected_instance_host: Optional[str] = None
|
| 25 |
+
command_history: List[Dict] = field(default_factory=list)
|
| 26 |
+
workflows: List[Dict] = field(default_factory=list)
|
| 27 |
+
preferences: Dict = field(default_factory=dict)
|
| 28 |
+
|
| 29 |
+
def to_dict(self) -> dict:
|
| 30 |
+
return asdict(self)
|
| 31 |
+
|
| 32 |
+
@classmethod
|
| 33 |
+
def from_dict(cls, data: dict) -> "Session":
|
| 34 |
+
return cls(**data)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class SessionManager:
|
| 38 |
+
"""Manages session persistence and state"""
|
| 39 |
+
|
| 40 |
+
def __init__(self, storage_dir: str = None):
|
| 41 |
+
if storage_dir is None:
|
| 42 |
+
storage_dir = os.path.join(tempfile.gettempdir(), "kariana_ui_sessions")
|
| 43 |
+
self.storage_dir = Path(storage_dir)
|
| 44 |
+
self.storage_dir.mkdir(parents=True, exist_ok=True)
|
| 45 |
+
self._current_session: Optional[Session] = None
|
| 46 |
+
|
| 47 |
+
def _generate_session_id(self) -> str:
|
| 48 |
+
"""Generate unique session ID"""
|
| 49 |
+
import uuid
|
| 50 |
+
return str(uuid.uuid4())[:8]
|
| 51 |
+
|
| 52 |
+
def _get_session_path(self, session_id: str) -> Path:
|
| 53 |
+
"""Get path to session file"""
|
| 54 |
+
return self.storage_dir / f"session_{session_id}.json"
|
| 55 |
+
|
| 56 |
+
def create_session(self) -> Session:
|
| 57 |
+
"""Create a new session"""
|
| 58 |
+
now = datetime.utcnow().isoformat()
|
| 59 |
+
session = Session(
|
| 60 |
+
session_id=self._generate_session_id(),
|
| 61 |
+
created_at=now,
|
| 62 |
+
last_activity=now,
|
| 63 |
+
)
|
| 64 |
+
self._current_session = session
|
| 65 |
+
self._save_session(session)
|
| 66 |
+
logger.info(f"Created session {session.session_id}")
|
| 67 |
+
return session
|
| 68 |
+
|
| 69 |
+
def get_or_create_session(self) -> Session:
|
| 70 |
+
"""Get current session or create new one"""
|
| 71 |
+
if self._current_session is None:
|
| 72 |
+
# Try to load most recent session
|
| 73 |
+
sessions = self._list_sessions()
|
| 74 |
+
if sessions:
|
| 75 |
+
self._current_session = self._load_session(sessions[-1])
|
| 76 |
+
else:
|
| 77 |
+
self._current_session = self.create_session()
|
| 78 |
+
return self._current_session
|
| 79 |
+
|
| 80 |
+
def _save_session(self, session: Session):
|
| 81 |
+
"""Save session to disk"""
|
| 82 |
+
try:
|
| 83 |
+
path = self._get_session_path(session.session_id)
|
| 84 |
+
with open(path, 'w') as f:
|
| 85 |
+
json.dump(session.to_dict(), f, indent=2)
|
| 86 |
+
except Exception as e:
|
| 87 |
+
logger.error(f"Failed to save session: {e}")
|
| 88 |
+
|
| 89 |
+
def _load_session(self, session_id: str) -> Optional[Session]:
|
| 90 |
+
"""Load session from disk"""
|
| 91 |
+
try:
|
| 92 |
+
path = self._get_session_path(session_id)
|
| 93 |
+
if path.exists():
|
| 94 |
+
with open(path, 'r') as f:
|
| 95 |
+
data = json.load(f)
|
| 96 |
+
return Session.from_dict(data)
|
| 97 |
+
except Exception as e:
|
| 98 |
+
logger.error(f"Failed to load session {session_id}: {e}")
|
| 99 |
+
return None
|
| 100 |
+
|
| 101 |
+
def _list_sessions(self) -> List[str]:
|
| 102 |
+
"""List all session IDs"""
|
| 103 |
+
sessions = []
|
| 104 |
+
for path in self.storage_dir.glob("session_*.json"):
|
| 105 |
+
session_id = path.stem.replace("session_", "")
|
| 106 |
+
sessions.append(session_id)
|
| 107 |
+
return sorted(sessions)
|
| 108 |
+
|
| 109 |
+
def update_activity(self):
|
| 110 |
+
"""Update last activity timestamp"""
|
| 111 |
+
if self._current_session:
|
| 112 |
+
self._current_session.last_activity = datetime.utcnow().isoformat()
|
| 113 |
+
self._save_session(self._current_session)
|
| 114 |
+
|
| 115 |
+
def set_connected_instance(self, host: str, port: int):
|
| 116 |
+
"""Record connected instance"""
|
| 117 |
+
session = self.get_or_create_session()
|
| 118 |
+
session.connected_instance_host = host
|
| 119 |
+
session.connected_instance_port = port
|
| 120 |
+
self._save_session(session)
|
| 121 |
+
|
| 122 |
+
def add_command_to_history(self, command: Dict, response: Dict = None):
|
| 123 |
+
"""Add command to history"""
|
| 124 |
+
session = self.get_or_create_session()
|
| 125 |
+
entry = {
|
| 126 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 127 |
+
"command": command,
|
| 128 |
+
"response": response,
|
| 129 |
+
}
|
| 130 |
+
session.command_history.append(entry)
|
| 131 |
+
# Keep last 100 commands
|
| 132 |
+
session.command_history = session.command_history[-100:]
|
| 133 |
+
self._save_session(session)
|
| 134 |
+
|
| 135 |
+
def get_command_history(self, limit: int = 50) -> List[Dict]:
|
| 136 |
+
"""Get recent command history"""
|
| 137 |
+
session = self.get_or_create_session()
|
| 138 |
+
return session.command_history[-limit:]
|
| 139 |
+
|
| 140 |
+
def save_workflow(self, workflow: Dict):
|
| 141 |
+
"""Save a workflow"""
|
| 142 |
+
session = self.get_or_create_session()
|
| 143 |
+
# Update if exists, otherwise append
|
| 144 |
+
for i, w in enumerate(session.workflows):
|
| 145 |
+
if w.get("name") == workflow.get("name"):
|
| 146 |
+
session.workflows[i] = workflow
|
| 147 |
+
self._save_session(session)
|
| 148 |
+
return
|
| 149 |
+
session.workflows.append(workflow)
|
| 150 |
+
self._save_session(session)
|
| 151 |
+
|
| 152 |
+
def get_workflows(self) -> List[Dict]:
|
| 153 |
+
"""Get saved workflows"""
|
| 154 |
+
session = self.get_or_create_session()
|
| 155 |
+
return session.workflows
|
| 156 |
+
|
| 157 |
+
def delete_workflow(self, name: str) -> bool:
|
| 158 |
+
"""Delete a workflow by name"""
|
| 159 |
+
session = self.get_or_create_session()
|
| 160 |
+
for i, w in enumerate(session.workflows):
|
| 161 |
+
if w.get("name") == name:
|
| 162 |
+
session.workflows.pop(i)
|
| 163 |
+
self._save_session(session)
|
| 164 |
+
return True
|
| 165 |
+
return False
|
| 166 |
+
|
| 167 |
+
def set_preference(self, key: str, value):
|
| 168 |
+
"""Set a preference"""
|
| 169 |
+
session = self.get_or_create_session()
|
| 170 |
+
session.preferences[key] = value
|
| 171 |
+
self._save_session(session)
|
| 172 |
+
|
| 173 |
+
def get_preference(self, key: str, default=None):
|
| 174 |
+
"""Get a preference"""
|
| 175 |
+
session = self.get_or_create_session()
|
| 176 |
+
return session.preferences.get(key, default)
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
# Global session manager
|
| 180 |
+
_session_manager: Optional[SessionManager] = None
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def get_session_manager() -> SessionManager:
|
| 184 |
+
"""Get or create global session manager"""
|
| 185 |
+
global _session_manager
|
| 186 |
+
if _session_manager is None:
|
| 187 |
+
_session_manager = SessionManager()
|
| 188 |
+
return _session_manager
|
tabs/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Kariana Unified UI - Tabs Module
|
| 2 |
+
from .monitoring import create_monitoring_tab
|
| 3 |
+
from .scene_control import create_scene_control_tab
|
| 4 |
+
from .skills_agents import create_skills_agents_tab
|
| 5 |
+
from .workflow_builder import create_workflow_builder_tab
|
| 6 |
+
from .setup import create_setup_tab
|
| 7 |
+
|
| 8 |
+
__all__ = [
|
| 9 |
+
'create_monitoring_tab',
|
| 10 |
+
'create_scene_control_tab',
|
| 11 |
+
'create_skills_agents_tab',
|
| 12 |
+
'create_workflow_builder_tab',
|
| 13 |
+
'create_setup_tab'
|
| 14 |
+
]
|
tabs/monitoring.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Kariana Unified UI - Monitoring Tab
|
| 3 |
+
Real-time monitoring of Unreal Engine connection, logs, and performance
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import gradio as gr
|
| 7 |
+
import logging
|
| 8 |
+
from typing import Dict, List, Optional
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
# Import will work once the package is properly set up
|
| 14 |
+
try:
|
| 15 |
+
from ..core.connection import get_connection
|
| 16 |
+
except ImportError:
|
| 17 |
+
get_connection = None
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def create_monitoring_tab() -> Dict:
|
| 21 |
+
"""Create the Monitoring tab"""
|
| 22 |
+
|
| 23 |
+
with gr.Column():
|
| 24 |
+
# Connection Status Card
|
| 25 |
+
gr.Markdown("## Connection Status")
|
| 26 |
+
|
| 27 |
+
with gr.Row():
|
| 28 |
+
with gr.Column(scale=1):
|
| 29 |
+
status_indicator = gr.Markdown("**Status:** Checking...")
|
| 30 |
+
|
| 31 |
+
with gr.Column(scale=2):
|
| 32 |
+
with gr.Row():
|
| 33 |
+
instance_info = gr.JSON(
|
| 34 |
+
label="Instance Info",
|
| 35 |
+
value={"status": "not_connected"}
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
# Quick Actions
|
| 39 |
+
with gr.Row():
|
| 40 |
+
ping_btn = gr.Button("Ping", size="sm")
|
| 41 |
+
refresh_info_btn = gr.Button("Refresh Info", size="sm")
|
| 42 |
+
|
| 43 |
+
ping_result = gr.Textbox(label="Ping Result", interactive=False)
|
| 44 |
+
|
| 45 |
+
gr.Markdown("---")
|
| 46 |
+
|
| 47 |
+
# Performance Metrics
|
| 48 |
+
gr.Markdown("## Performance Metrics")
|
| 49 |
+
|
| 50 |
+
with gr.Row():
|
| 51 |
+
with gr.Column():
|
| 52 |
+
fps_display = gr.Number(label="FPS", value=0, interactive=False)
|
| 53 |
+
with gr.Column():
|
| 54 |
+
frame_time_display = gr.Number(label="Frame Time (ms)", value=0, interactive=False)
|
| 55 |
+
with gr.Column():
|
| 56 |
+
draw_calls_display = gr.Number(label="Draw Calls", value=0, interactive=False)
|
| 57 |
+
|
| 58 |
+
refresh_perf_btn = gr.Button("Refresh Metrics", size="sm")
|
| 59 |
+
|
| 60 |
+
gr.Markdown("---")
|
| 61 |
+
|
| 62 |
+
# Real-time Logs
|
| 63 |
+
gr.Markdown("## Unreal Engine Logs")
|
| 64 |
+
|
| 65 |
+
with gr.Row():
|
| 66 |
+
log_category = gr.Dropdown(
|
| 67 |
+
label="Category Filter",
|
| 68 |
+
choices=["All", "LogTemp", "LogPython", "LogBlueprintUserMessages", "LogKariana"],
|
| 69 |
+
value="All",
|
| 70 |
+
scale=1
|
| 71 |
+
)
|
| 72 |
+
log_limit = gr.Slider(
|
| 73 |
+
label="Max Lines",
|
| 74 |
+
minimum=10,
|
| 75 |
+
maximum=500,
|
| 76 |
+
value=100,
|
| 77 |
+
step=10,
|
| 78 |
+
scale=1
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
with gr.Row():
|
| 82 |
+
fetch_logs_btn = gr.Button("Fetch Logs", size="sm")
|
| 83 |
+
clear_logs_btn = gr.Button("Clear", size="sm")
|
| 84 |
+
auto_refresh = gr.Checkbox(label="Auto-refresh (5s)", value=False)
|
| 85 |
+
|
| 86 |
+
logs_display = gr.TextArea(
|
| 87 |
+
label="Log Output",
|
| 88 |
+
lines=15,
|
| 89 |
+
max_lines=30,
|
| 90 |
+
interactive=False,
|
| 91 |
+
show_copy_button=True
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
# Event Handlers
|
| 95 |
+
def handle_ping() -> str:
|
| 96 |
+
"""Ping the connected instance"""
|
| 97 |
+
try:
|
| 98 |
+
if get_connection is None:
|
| 99 |
+
return "Connection module not available"
|
| 100 |
+
|
| 101 |
+
conn = get_connection()
|
| 102 |
+
if not conn.is_connected:
|
| 103 |
+
return "Not connected - enter PIN in Setup tab"
|
| 104 |
+
|
| 105 |
+
start = datetime.now()
|
| 106 |
+
if conn.ping():
|
| 107 |
+
elapsed = (datetime.now() - start).total_seconds() * 1000
|
| 108 |
+
return f"Pong! Response time: {elapsed:.1f}ms"
|
| 109 |
+
return "Ping failed - no response"
|
| 110 |
+
except Exception as e:
|
| 111 |
+
return f"Error: {str(e)}"
|
| 112 |
+
|
| 113 |
+
def handle_refresh_info() -> tuple:
|
| 114 |
+
"""Refresh connection info"""
|
| 115 |
+
try:
|
| 116 |
+
if get_connection is None:
|
| 117 |
+
return "**Status:** Module not available", {"error": "not_available"}
|
| 118 |
+
|
| 119 |
+
conn = get_connection()
|
| 120 |
+
if not conn.is_connected:
|
| 121 |
+
return "**Status:** Not Connected", {"status": "not_connected"}
|
| 122 |
+
|
| 123 |
+
info = conn.get_server_info()
|
| 124 |
+
if info:
|
| 125 |
+
status = f"**Status:** Connected to {info.get('project', 'Unknown')} (Port {info.get('port', 'N/A')})"
|
| 126 |
+
return status, info
|
| 127 |
+
return "**Status:** Connected (no info)", {"status": "connected"}
|
| 128 |
+
except Exception as e:
|
| 129 |
+
return f"**Status:** Error - {str(e)}", {"error": str(e)}
|
| 130 |
+
|
| 131 |
+
def handle_refresh_performance() -> tuple:
|
| 132 |
+
"""Refresh performance metrics"""
|
| 133 |
+
try:
|
| 134 |
+
if get_connection is None:
|
| 135 |
+
return 0, 0, 0
|
| 136 |
+
|
| 137 |
+
conn = get_connection()
|
| 138 |
+
if not conn.is_connected:
|
| 139 |
+
return 0, 0, 0
|
| 140 |
+
|
| 141 |
+
metrics = conn.get_performance_metrics()
|
| 142 |
+
if metrics and metrics.get("success"):
|
| 143 |
+
return (
|
| 144 |
+
metrics.get("fps", 0),
|
| 145 |
+
metrics.get("frame_time_ms", 0),
|
| 146 |
+
metrics.get("draw_calls", 0)
|
| 147 |
+
)
|
| 148 |
+
return 0, 0, 0
|
| 149 |
+
except Exception as e:
|
| 150 |
+
logger.error(f"Performance metrics error: {e}")
|
| 151 |
+
return 0, 0, 0
|
| 152 |
+
|
| 153 |
+
def handle_fetch_logs(category: str, limit: int) -> str:
|
| 154 |
+
"""Fetch logs from Unreal"""
|
| 155 |
+
try:
|
| 156 |
+
if get_connection is None:
|
| 157 |
+
return "Connection module not available"
|
| 158 |
+
|
| 159 |
+
conn = get_connection()
|
| 160 |
+
if not conn.is_connected:
|
| 161 |
+
return "Not connected - enter PIN in Setup tab"
|
| 162 |
+
|
| 163 |
+
cat_filter = None if category == "All" else category
|
| 164 |
+
logs = conn.get_logs(limit=int(limit), category=cat_filter)
|
| 165 |
+
|
| 166 |
+
if logs:
|
| 167 |
+
return "\n".join(logs)
|
| 168 |
+
return "No logs available"
|
| 169 |
+
except Exception as e:
|
| 170 |
+
return f"Error fetching logs: {str(e)}"
|
| 171 |
+
|
| 172 |
+
# Wire up event handlers
|
| 173 |
+
ping_btn.click(
|
| 174 |
+
fn=handle_ping,
|
| 175 |
+
outputs=[ping_result]
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
refresh_info_btn.click(
|
| 179 |
+
fn=handle_refresh_info,
|
| 180 |
+
outputs=[status_indicator, instance_info]
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
refresh_perf_btn.click(
|
| 184 |
+
fn=handle_refresh_performance,
|
| 185 |
+
outputs=[fps_display, frame_time_display, draw_calls_display]
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
fetch_logs_btn.click(
|
| 189 |
+
fn=handle_fetch_logs,
|
| 190 |
+
inputs=[log_category, log_limit],
|
| 191 |
+
outputs=[logs_display]
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
clear_logs_btn.click(
|
| 195 |
+
fn=lambda: "",
|
| 196 |
+
outputs=[logs_display]
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
return {
|
| 200 |
+
"status_indicator": status_indicator,
|
| 201 |
+
"instance_info": instance_info,
|
| 202 |
+
"ping_result": ping_result,
|
| 203 |
+
"fps_display": fps_display,
|
| 204 |
+
"frame_time_display": frame_time_display,
|
| 205 |
+
"draw_calls_display": draw_calls_display,
|
| 206 |
+
"logs_display": logs_display,
|
| 207 |
+
}
|
tabs/scene_control.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Kariana Unified UI - Scene Control Tab
|
| 3 |
+
Visual scene manipulation: actor browser, transforms, materials
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import gradio as gr
|
| 7 |
+
import logging
|
| 8 |
+
from typing import Dict, List, Optional
|
| 9 |
+
import json
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
try:
|
| 14 |
+
from ..core.connection import get_connection
|
| 15 |
+
except ImportError:
|
| 16 |
+
get_connection = None
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def create_scene_control_tab() -> Dict:
|
| 20 |
+
"""Create the Scene Control tab"""
|
| 21 |
+
|
| 22 |
+
with gr.Column():
|
| 23 |
+
gr.Markdown("## Scene Control")
|
| 24 |
+
|
| 25 |
+
with gr.Row():
|
| 26 |
+
# Left Panel: Actor Browser
|
| 27 |
+
with gr.Column(scale=1):
|
| 28 |
+
gr.Markdown("### World Outliner")
|
| 29 |
+
|
| 30 |
+
with gr.Row():
|
| 31 |
+
refresh_actors_btn = gr.Button("Refresh", size="sm")
|
| 32 |
+
actor_search = gr.Textbox(
|
| 33 |
+
placeholder="Search actors...",
|
| 34 |
+
show_label=False,
|
| 35 |
+
scale=2
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
actor_list = gr.Dataframe(
|
| 39 |
+
headers=["Name", "Type", "Folder"],
|
| 40 |
+
datatype=["str", "str", "str"],
|
| 41 |
+
interactive=False,
|
| 42 |
+
label="Actors",
|
| 43 |
+
max_rows=20
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
selected_actor = gr.Textbox(
|
| 47 |
+
label="Selected Actor",
|
| 48 |
+
interactive=False
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
# Right Panel: Properties
|
| 52 |
+
with gr.Column(scale=1):
|
| 53 |
+
gr.Markdown("### Transform")
|
| 54 |
+
|
| 55 |
+
with gr.Group():
|
| 56 |
+
gr.Markdown("**Location**")
|
| 57 |
+
with gr.Row():
|
| 58 |
+
loc_x = gr.Number(label="X", value=0, scale=1)
|
| 59 |
+
loc_y = gr.Number(label="Y", value=0, scale=1)
|
| 60 |
+
loc_z = gr.Number(label="Z", value=0, scale=1)
|
| 61 |
+
|
| 62 |
+
with gr.Group():
|
| 63 |
+
gr.Markdown("**Rotation**")
|
| 64 |
+
with gr.Row():
|
| 65 |
+
rot_x = gr.Number(label="Pitch", value=0, scale=1)
|
| 66 |
+
rot_y = gr.Number(label="Yaw", value=0, scale=1)
|
| 67 |
+
rot_z = gr.Number(label="Roll", value=0, scale=1)
|
| 68 |
+
|
| 69 |
+
with gr.Group():
|
| 70 |
+
gr.Markdown("**Scale**")
|
| 71 |
+
with gr.Row():
|
| 72 |
+
scale_x = gr.Number(label="X", value=1, scale=1)
|
| 73 |
+
scale_y = gr.Number(label="Y", value=1, scale=1)
|
| 74 |
+
scale_z = gr.Number(label="Z", value=1, scale=1)
|
| 75 |
+
|
| 76 |
+
with gr.Row():
|
| 77 |
+
get_transform_btn = gr.Button("Get Transform", size="sm")
|
| 78 |
+
set_transform_btn = gr.Button("Set Transform", variant="primary", size="sm")
|
| 79 |
+
|
| 80 |
+
transform_result = gr.Markdown("")
|
| 81 |
+
|
| 82 |
+
gr.Markdown("---")
|
| 83 |
+
|
| 84 |
+
# Quick Actions
|
| 85 |
+
gr.Markdown("### Quick Actions")
|
| 86 |
+
|
| 87 |
+
with gr.Row():
|
| 88 |
+
with gr.Column(scale=1):
|
| 89 |
+
gr.Markdown("**Spawn Actor**")
|
| 90 |
+
actor_type = gr.Dropdown(
|
| 91 |
+
label="Type",
|
| 92 |
+
choices=[
|
| 93 |
+
"Cube",
|
| 94 |
+
"Sphere",
|
| 95 |
+
"Cylinder",
|
| 96 |
+
"Cone",
|
| 97 |
+
"PointLight",
|
| 98 |
+
"SpotLight",
|
| 99 |
+
"DirectionalLight",
|
| 100 |
+
"Camera",
|
| 101 |
+
"Empty"
|
| 102 |
+
],
|
| 103 |
+
value="Cube"
|
| 104 |
+
)
|
| 105 |
+
actor_name = gr.Textbox(label="Name", placeholder="MyActor")
|
| 106 |
+
spawn_btn = gr.Button("Spawn", variant="primary")
|
| 107 |
+
|
| 108 |
+
with gr.Column(scale=1):
|
| 109 |
+
gr.Markdown("**Delete Actor**")
|
| 110 |
+
delete_name = gr.Textbox(label="Actor Name", placeholder="ActorToDelete")
|
| 111 |
+
delete_btn = gr.Button("Delete", variant="stop")
|
| 112 |
+
|
| 113 |
+
spawn_result = gr.Markdown("")
|
| 114 |
+
|
| 115 |
+
gr.Markdown("---")
|
| 116 |
+
|
| 117 |
+
# Screenshot Preview
|
| 118 |
+
gr.Markdown("### Scene Preview")
|
| 119 |
+
with gr.Row():
|
| 120 |
+
capture_btn = gr.Button("Capture Screenshot", size="sm")
|
| 121 |
+
|
| 122 |
+
screenshot_display = gr.Image(label="Viewport Screenshot", type="filepath")
|
| 123 |
+
|
| 124 |
+
# Event Handlers
|
| 125 |
+
def handle_refresh_actors(search: str = "") -> List[List]:
|
| 126 |
+
"""Refresh actor list"""
|
| 127 |
+
try:
|
| 128 |
+
if get_connection is None:
|
| 129 |
+
return [["N/A", "Module not available", ""]]
|
| 130 |
+
|
| 131 |
+
conn = get_connection()
|
| 132 |
+
if not conn.is_connected:
|
| 133 |
+
return [["N/A", "Not connected", ""]]
|
| 134 |
+
|
| 135 |
+
actors = conn.list_actors()
|
| 136 |
+
if not actors:
|
| 137 |
+
return [["No actors", "", ""]]
|
| 138 |
+
|
| 139 |
+
rows = []
|
| 140 |
+
for actor in actors:
|
| 141 |
+
name = actor.get("name", "Unknown")
|
| 142 |
+
actor_type = actor.get("type", actor.get("class", "Unknown"))
|
| 143 |
+
folder = actor.get("folder", "")
|
| 144 |
+
|
| 145 |
+
# Apply search filter
|
| 146 |
+
if search and search.lower() not in name.lower():
|
| 147 |
+
continue
|
| 148 |
+
|
| 149 |
+
rows.append([name, actor_type, folder])
|
| 150 |
+
|
| 151 |
+
return rows if rows else [["No matching actors", "", ""]]
|
| 152 |
+
except Exception as e:
|
| 153 |
+
logger.error(f"Actor list error: {e}")
|
| 154 |
+
return [[f"Error: {str(e)}", "", ""]]
|
| 155 |
+
|
| 156 |
+
def handle_select_actor(evt: gr.SelectData, data) -> str:
|
| 157 |
+
"""Handle actor selection from table"""
|
| 158 |
+
try:
|
| 159 |
+
if evt.index is not None and len(evt.index) >= 1:
|
| 160 |
+
row_idx = evt.index[0]
|
| 161 |
+
if data and len(data) > row_idx:
|
| 162 |
+
return data[row_idx][0] # Return actor name
|
| 163 |
+
except:
|
| 164 |
+
pass
|
| 165 |
+
return ""
|
| 166 |
+
|
| 167 |
+
def handle_get_transform(actor_name: str) -> tuple:
|
| 168 |
+
"""Get transform of selected actor"""
|
| 169 |
+
if not actor_name:
|
| 170 |
+
return 0, 0, 0, 0, 0, 0, 1, 1, 1, "Select an actor first"
|
| 171 |
+
|
| 172 |
+
try:
|
| 173 |
+
if get_connection is None:
|
| 174 |
+
return 0, 0, 0, 0, 0, 0, 1, 1, 1, "Module not available"
|
| 175 |
+
|
| 176 |
+
conn = get_connection()
|
| 177 |
+
if not conn.is_connected:
|
| 178 |
+
return 0, 0, 0, 0, 0, 0, 1, 1, 1, "Not connected"
|
| 179 |
+
|
| 180 |
+
result = conn.get_actor_transform(actor_name)
|
| 181 |
+
if result and result.get("success"):
|
| 182 |
+
loc = result.get("location", [0, 0, 0])
|
| 183 |
+
rot = result.get("rotation", [0, 0, 0])
|
| 184 |
+
scale = result.get("scale", [1, 1, 1])
|
| 185 |
+
return (
|
| 186 |
+
loc[0], loc[1], loc[2],
|
| 187 |
+
rot[0], rot[1], rot[2],
|
| 188 |
+
scale[0], scale[1], scale[2],
|
| 189 |
+
f"Transform loaded for {actor_name}"
|
| 190 |
+
)
|
| 191 |
+
return 0, 0, 0, 0, 0, 0, 1, 1, 1, f"Failed to get transform: {result.get('error', 'Unknown error')}"
|
| 192 |
+
except Exception as e:
|
| 193 |
+
return 0, 0, 0, 0, 0, 0, 1, 1, 1, f"Error: {str(e)}"
|
| 194 |
+
|
| 195 |
+
def handle_set_transform(
|
| 196 |
+
actor_name: str,
|
| 197 |
+
lx, ly, lz,
|
| 198 |
+
rx, ry, rz,
|
| 199 |
+
sx, sy, sz
|
| 200 |
+
) -> str:
|
| 201 |
+
"""Set transform of selected actor"""
|
| 202 |
+
if not actor_name:
|
| 203 |
+
return "Select an actor first"
|
| 204 |
+
|
| 205 |
+
try:
|
| 206 |
+
if get_connection is None:
|
| 207 |
+
return "Module not available"
|
| 208 |
+
|
| 209 |
+
conn = get_connection()
|
| 210 |
+
if not conn.is_connected:
|
| 211 |
+
return "Not connected"
|
| 212 |
+
|
| 213 |
+
result = conn.set_actor_transform(
|
| 214 |
+
actor_name,
|
| 215 |
+
location=[lx, ly, lz],
|
| 216 |
+
rotation=[rx, ry, rz],
|
| 217 |
+
scale=[sx, sy, sz]
|
| 218 |
+
)
|
| 219 |
+
if result and result.get("success"):
|
| 220 |
+
return f"Transform updated for {actor_name}"
|
| 221 |
+
return f"Failed: {result.get('error', 'Unknown error')}"
|
| 222 |
+
except Exception as e:
|
| 223 |
+
return f"Error: {str(e)}"
|
| 224 |
+
|
| 225 |
+
def handle_spawn(actor_type: str, name: str) -> str:
|
| 226 |
+
"""Spawn new actor"""
|
| 227 |
+
if not name:
|
| 228 |
+
return "Enter an actor name"
|
| 229 |
+
|
| 230 |
+
try:
|
| 231 |
+
if get_connection is None:
|
| 232 |
+
return "Module not available"
|
| 233 |
+
|
| 234 |
+
conn = get_connection()
|
| 235 |
+
if not conn.is_connected:
|
| 236 |
+
return "Not connected"
|
| 237 |
+
|
| 238 |
+
result = conn.spawn_actor(actor_type, name)
|
| 239 |
+
if result and result.get("success"):
|
| 240 |
+
return f"Spawned {actor_type}: {name}"
|
| 241 |
+
return f"Failed: {result.get('error', 'Unknown error')}"
|
| 242 |
+
except Exception as e:
|
| 243 |
+
return f"Error: {str(e)}"
|
| 244 |
+
|
| 245 |
+
def handle_delete(name: str) -> str:
|
| 246 |
+
"""Delete actor"""
|
| 247 |
+
if not name:
|
| 248 |
+
return "Enter actor name to delete"
|
| 249 |
+
|
| 250 |
+
try:
|
| 251 |
+
if get_connection is None:
|
| 252 |
+
return "Module not available"
|
| 253 |
+
|
| 254 |
+
conn = get_connection()
|
| 255 |
+
if not conn.is_connected:
|
| 256 |
+
return "Not connected"
|
| 257 |
+
|
| 258 |
+
result = conn.delete_actor(name)
|
| 259 |
+
if result and result.get("success"):
|
| 260 |
+
return f"Deleted: {name}"
|
| 261 |
+
return f"Failed: {result.get('error', 'Unknown error')}"
|
| 262 |
+
except Exception as e:
|
| 263 |
+
return f"Error: {str(e)}"
|
| 264 |
+
|
| 265 |
+
def handle_capture() -> Optional[str]:
|
| 266 |
+
"""Capture screenshot"""
|
| 267 |
+
try:
|
| 268 |
+
if get_connection is None:
|
| 269 |
+
return None
|
| 270 |
+
|
| 271 |
+
conn = get_connection()
|
| 272 |
+
if not conn.is_connected:
|
| 273 |
+
return None
|
| 274 |
+
|
| 275 |
+
result = conn.capture_screenshot()
|
| 276 |
+
if result and result.get("success"):
|
| 277 |
+
return result.get("path")
|
| 278 |
+
return None
|
| 279 |
+
except Exception as e:
|
| 280 |
+
logger.error(f"Screenshot error: {e}")
|
| 281 |
+
return None
|
| 282 |
+
|
| 283 |
+
# Wire up event handlers
|
| 284 |
+
refresh_actors_btn.click(
|
| 285 |
+
fn=handle_refresh_actors,
|
| 286 |
+
inputs=[actor_search],
|
| 287 |
+
outputs=[actor_list]
|
| 288 |
+
)
|
| 289 |
+
|
| 290 |
+
actor_search.change(
|
| 291 |
+
fn=handle_refresh_actors,
|
| 292 |
+
inputs=[actor_search],
|
| 293 |
+
outputs=[actor_list]
|
| 294 |
+
)
|
| 295 |
+
|
| 296 |
+
actor_list.select(
|
| 297 |
+
fn=handle_select_actor,
|
| 298 |
+
inputs=[actor_list],
|
| 299 |
+
outputs=[selected_actor]
|
| 300 |
+
)
|
| 301 |
+
|
| 302 |
+
get_transform_btn.click(
|
| 303 |
+
fn=handle_get_transform,
|
| 304 |
+
inputs=[selected_actor],
|
| 305 |
+
outputs=[loc_x, loc_y, loc_z, rot_x, rot_y, rot_z, scale_x, scale_y, scale_z, transform_result]
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
set_transform_btn.click(
|
| 309 |
+
fn=handle_set_transform,
|
| 310 |
+
inputs=[selected_actor, loc_x, loc_y, loc_z, rot_x, rot_y, rot_z, scale_x, scale_y, scale_z],
|
| 311 |
+
outputs=[transform_result]
|
| 312 |
+
)
|
| 313 |
+
|
| 314 |
+
spawn_btn.click(
|
| 315 |
+
fn=handle_spawn,
|
| 316 |
+
inputs=[actor_type, actor_name],
|
| 317 |
+
outputs=[spawn_result]
|
| 318 |
+
)
|
| 319 |
+
|
| 320 |
+
delete_btn.click(
|
| 321 |
+
fn=handle_delete,
|
| 322 |
+
inputs=[delete_name],
|
| 323 |
+
outputs=[spawn_result]
|
| 324 |
+
)
|
| 325 |
+
|
| 326 |
+
capture_btn.click(
|
| 327 |
+
fn=handle_capture,
|
| 328 |
+
outputs=[screenshot_display]
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
+
return {
|
| 332 |
+
"actor_list": actor_list,
|
| 333 |
+
"selected_actor": selected_actor,
|
| 334 |
+
"screenshot_display": screenshot_display,
|
| 335 |
+
}
|
tabs/setup.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Kariana Unified UI - Setup Tab
|
| 3 |
+
Handles connection setup, PIN authentication, and configuration
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import gradio as gr
|
| 7 |
+
import logging
|
| 8 |
+
from typing import Dict, List, Optional, Tuple
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
# Import will work once the package is properly set up
|
| 13 |
+
try:
|
| 14 |
+
from ..core.connection import get_connection
|
| 15 |
+
from ..core.auth import get_authenticator
|
| 16 |
+
from ..utils.discovery import discover_instances, UnrealInstance
|
| 17 |
+
except ImportError:
|
| 18 |
+
# Fallback for development
|
| 19 |
+
get_connection = None
|
| 20 |
+
get_authenticator = None
|
| 21 |
+
discover_instances = None
|
| 22 |
+
UnrealInstance = None
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def create_setup_tab() -> Dict:
|
| 26 |
+
"""Create the Setup & Configuration tab"""
|
| 27 |
+
|
| 28 |
+
with gr.Column():
|
| 29 |
+
gr.Markdown("## Connection Setup")
|
| 30 |
+
gr.Markdown("Enter the 4-digit PIN shown in Unreal Editor to connect.")
|
| 31 |
+
|
| 32 |
+
# PIN Input Section
|
| 33 |
+
with gr.Row():
|
| 34 |
+
with gr.Column(scale=2):
|
| 35 |
+
pin_input = gr.Textbox(
|
| 36 |
+
label="Connection PIN",
|
| 37 |
+
placeholder="Enter 4-digit PIN",
|
| 38 |
+
max_lines=1,
|
| 39 |
+
elem_classes=["pin-input"]
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
with gr.Column(scale=1):
|
| 43 |
+
connect_btn = gr.Button("Connect", variant="primary", size="lg")
|
| 44 |
+
disconnect_btn = gr.Button("Disconnect", variant="secondary")
|
| 45 |
+
|
| 46 |
+
connection_result = gr.Markdown("")
|
| 47 |
+
|
| 48 |
+
gr.Markdown("---")
|
| 49 |
+
|
| 50 |
+
# Instance Discovery Section
|
| 51 |
+
gr.Markdown("### Discovered Instances")
|
| 52 |
+
with gr.Row():
|
| 53 |
+
discover_btn = gr.Button("Discover Instances", size="sm")
|
| 54 |
+
clear_btn = gr.Button("Clear", size="sm")
|
| 55 |
+
|
| 56 |
+
instances_table = gr.Dataframe(
|
| 57 |
+
headers=["Port", "Project", "Instance ID", "Status"],
|
| 58 |
+
datatype=["number", "str", "str", "str"],
|
| 59 |
+
interactive=False,
|
| 60 |
+
label="Running Instances"
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
gr.Markdown("---")
|
| 64 |
+
|
| 65 |
+
# Configuration Section
|
| 66 |
+
gr.Markdown("### Configuration")
|
| 67 |
+
|
| 68 |
+
with gr.Accordion("Advanced Settings", open=False):
|
| 69 |
+
with gr.Row():
|
| 70 |
+
host_input = gr.Textbox(
|
| 71 |
+
label="Unreal Host",
|
| 72 |
+
value="localhost",
|
| 73 |
+
info="Host where Unreal Engine is running"
|
| 74 |
+
)
|
| 75 |
+
port_range_start = gr.Number(
|
| 76 |
+
label="Port Range Start",
|
| 77 |
+
value=9877,
|
| 78 |
+
precision=0
|
| 79 |
+
)
|
| 80 |
+
port_range_end = gr.Number(
|
| 81 |
+
label="Port Range End",
|
| 82 |
+
value=9887,
|
| 83 |
+
precision=0
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
timeout_input = gr.Slider(
|
| 87 |
+
label="Connection Timeout (seconds)",
|
| 88 |
+
minimum=1,
|
| 89 |
+
maximum=30,
|
| 90 |
+
value=5,
|
| 91 |
+
step=1
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
with gr.Accordion("Claude Desktop Config", open=False):
|
| 95 |
+
gr.Markdown("""
|
| 96 |
+
Copy this configuration to your Claude Desktop config file:
|
| 97 |
+
|
| 98 |
+
**macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
| 99 |
+
**Windows:** `%APPDATA%\\Claude\\claude_desktop_config.json`
|
| 100 |
+
""")
|
| 101 |
+
|
| 102 |
+
config_output = gr.Code(
|
| 103 |
+
label="Configuration JSON",
|
| 104 |
+
language="json",
|
| 105 |
+
value="""{
|
| 106 |
+
"mcpServers": {
|
| 107 |
+
"kariana-umcp": {
|
| 108 |
+
"command": "python",
|
| 109 |
+
"args": ["-m", "kariana_mcp_server"],
|
| 110 |
+
"env": {
|
| 111 |
+
"UNREAL_HOST": "localhost",
|
| 112 |
+
"UNREAL_PORT": "9877"
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
}"""
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
generate_config_btn = gr.Button("Generate Config for Connected Instance")
|
| 120 |
+
|
| 121 |
+
# Event Handlers
|
| 122 |
+
def handle_connect(pin: str) -> str:
|
| 123 |
+
"""Handle PIN connection"""
|
| 124 |
+
if not pin or len(pin) != 4 or not pin.isdigit():
|
| 125 |
+
return "**Error:** Please enter a valid 4-digit PIN"
|
| 126 |
+
|
| 127 |
+
try:
|
| 128 |
+
if get_connection is None:
|
| 129 |
+
return "**Error:** Connection module not available"
|
| 130 |
+
|
| 131 |
+
conn = get_connection()
|
| 132 |
+
if conn.connect_with_pin(pin):
|
| 133 |
+
instance = conn.instance
|
| 134 |
+
return f"**Connected!** Project: {instance.project_name}, Port: {instance.port}"
|
| 135 |
+
else:
|
| 136 |
+
return "**Error:** Invalid PIN or no instances found"
|
| 137 |
+
except Exception as e:
|
| 138 |
+
logger.error(f"Connection error: {e}")
|
| 139 |
+
return f"**Error:** {str(e)}"
|
| 140 |
+
|
| 141 |
+
def handle_disconnect() -> str:
|
| 142 |
+
"""Handle disconnect"""
|
| 143 |
+
try:
|
| 144 |
+
if get_connection is None:
|
| 145 |
+
return "Connection module not available"
|
| 146 |
+
|
| 147 |
+
conn = get_connection()
|
| 148 |
+
conn.disconnect()
|
| 149 |
+
return "**Disconnected**"
|
| 150 |
+
except Exception as e:
|
| 151 |
+
return f"**Error:** {str(e)}"
|
| 152 |
+
|
| 153 |
+
def handle_discover() -> List[List]:
|
| 154 |
+
"""Discover running instances"""
|
| 155 |
+
try:
|
| 156 |
+
if discover_instances is None:
|
| 157 |
+
return [["N/A", "Discovery not available", "N/A", "Error"]]
|
| 158 |
+
|
| 159 |
+
instances = discover_instances()
|
| 160 |
+
if not instances:
|
| 161 |
+
return [["N/A", "No instances found", "N/A", "Not Running"]]
|
| 162 |
+
|
| 163 |
+
rows = []
|
| 164 |
+
for inst in instances:
|
| 165 |
+
rows.append([
|
| 166 |
+
inst.port,
|
| 167 |
+
inst.project_name or "Unknown",
|
| 168 |
+
inst.instance_id[:16] + "..." if inst.instance_id else "N/A",
|
| 169 |
+
"Connected" if inst.authenticated else "Available"
|
| 170 |
+
])
|
| 171 |
+
return rows
|
| 172 |
+
except Exception as e:
|
| 173 |
+
logger.error(f"Discovery error: {e}")
|
| 174 |
+
return [["N/A", f"Error: {str(e)}", "N/A", "Error"]]
|
| 175 |
+
|
| 176 |
+
def generate_claude_config() -> str:
|
| 177 |
+
"""Generate Claude Desktop config for connected instance"""
|
| 178 |
+
try:
|
| 179 |
+
if get_connection is None:
|
| 180 |
+
return '{"error": "Connection module not available"}'
|
| 181 |
+
|
| 182 |
+
conn = get_connection()
|
| 183 |
+
if not conn.is_connected:
|
| 184 |
+
return '{"error": "Not connected to any instance"}'
|
| 185 |
+
|
| 186 |
+
config = {
|
| 187 |
+
"mcpServers": {
|
| 188 |
+
"kariana-umcp": {
|
| 189 |
+
"command": "python",
|
| 190 |
+
"args": ["-m", "kariana_mcp_server"],
|
| 191 |
+
"env": {
|
| 192 |
+
"UNREAL_HOST": conn.instance.host,
|
| 193 |
+
"UNREAL_PORT": str(conn.instance.port)
|
| 194 |
+
}
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
}
|
| 198 |
+
import json
|
| 199 |
+
return json.dumps(config, indent=2)
|
| 200 |
+
except Exception as e:
|
| 201 |
+
return f'{{"error": "{str(e)}"}}'
|
| 202 |
+
|
| 203 |
+
# Wire up event handlers
|
| 204 |
+
connect_btn.click(
|
| 205 |
+
fn=handle_connect,
|
| 206 |
+
inputs=[pin_input],
|
| 207 |
+
outputs=[connection_result]
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
disconnect_btn.click(
|
| 211 |
+
fn=handle_disconnect,
|
| 212 |
+
outputs=[connection_result]
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
discover_btn.click(
|
| 216 |
+
fn=handle_discover,
|
| 217 |
+
outputs=[instances_table]
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
clear_btn.click(
|
| 221 |
+
fn=lambda: [],
|
| 222 |
+
outputs=[instances_table]
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
generate_config_btn.click(
|
| 226 |
+
fn=generate_claude_config,
|
| 227 |
+
outputs=[config_output]
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
return {
|
| 231 |
+
"pin_input": pin_input,
|
| 232 |
+
"connect_btn": connect_btn,
|
| 233 |
+
"disconnect_btn": disconnect_btn,
|
| 234 |
+
"connection_result": connection_result,
|
| 235 |
+
"instances_table": instances_table,
|
| 236 |
+
}
|
tabs/skills_agents.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Kariana Unified UI - Skills & Agents Tab
|
| 3 |
+
Browse, execute, and monitor Claude skills and agents
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import gradio as gr
|
| 7 |
+
import logging
|
| 8 |
+
from typing import Dict, List, Optional
|
| 9 |
+
import json
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
try:
|
| 14 |
+
from ..core.connection import get_connection
|
| 15 |
+
except ImportError:
|
| 16 |
+
get_connection = None
|
| 17 |
+
|
| 18 |
+
# Available agents (from .claude/agents/)
|
| 19 |
+
AVAILABLE_AGENTS = [
|
| 20 |
+
{
|
| 21 |
+
"name": "Scene Analyst",
|
| 22 |
+
"file": "scene-analyst.md",
|
| 23 |
+
"description": "Transform natural language into structured scene specifications"
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
"name": "Asset Scout",
|
| 27 |
+
"file": "asset-scout.md",
|
| 28 |
+
"description": "Discover and catalog assets in Unreal project"
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"name": "Pattern Strategist",
|
| 32 |
+
"file": "pattern-strategist.md",
|
| 33 |
+
"description": "Select and configure spatial generation patterns"
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"name": "Executor",
|
| 37 |
+
"file": "executor.md",
|
| 38 |
+
"description": "Execute generation strategies using MCP tools"
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"name": "World Organizer",
|
| 42 |
+
"file": "world-organizer.md",
|
| 43 |
+
"description": "Automate World Outliner organization"
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"name": "Orchestrator",
|
| 47 |
+
"file": "orchestrator.md",
|
| 48 |
+
"description": "Coordinate multiple agents for complex tasks"
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"name": "MCP Log Monitor",
|
| 52 |
+
"file": "mcp-log-monitor.md",
|
| 53 |
+
"description": "Monitor MCP server logs and diagnose issues"
|
| 54 |
+
},
|
| 55 |
+
{
|
| 56 |
+
"name": "Project Organizer",
|
| 57 |
+
"file": "project-organizer.md",
|
| 58 |
+
"description": "Discover project structure and organize assets"
|
| 59 |
+
}
|
| 60 |
+
]
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def create_skills_agents_tab() -> Dict:
|
| 64 |
+
"""Create the Skills & Agents tab"""
|
| 65 |
+
|
| 66 |
+
with gr.Column():
|
| 67 |
+
with gr.Row():
|
| 68 |
+
# Left Panel: Skills Browser
|
| 69 |
+
with gr.Column(scale=1):
|
| 70 |
+
gr.Markdown("## Skills")
|
| 71 |
+
|
| 72 |
+
with gr.Row():
|
| 73 |
+
refresh_skills_btn = gr.Button("Refresh", size="sm")
|
| 74 |
+
skill_search = gr.Textbox(
|
| 75 |
+
placeholder="Search skills...",
|
| 76 |
+
show_label=False,
|
| 77 |
+
scale=2
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
skills_list = gr.Dataframe(
|
| 81 |
+
headers=["Name", "Description", "Version"],
|
| 82 |
+
datatype=["str", "str", "str"],
|
| 83 |
+
interactive=False,
|
| 84 |
+
label="Available Skills",
|
| 85 |
+
max_rows=15
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
selected_skill = gr.Textbox(
|
| 89 |
+
label="Selected Skill",
|
| 90 |
+
interactive=False
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
# Right Panel: Skill Details & Execution
|
| 94 |
+
with gr.Column(scale=1):
|
| 95 |
+
gr.Markdown("## Skill Details")
|
| 96 |
+
|
| 97 |
+
skill_info = gr.JSON(
|
| 98 |
+
label="Skill Information",
|
| 99 |
+
value={}
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
gr.Markdown("### Parameters")
|
| 103 |
+
skill_params = gr.Code(
|
| 104 |
+
label="Parameters (JSON)",
|
| 105 |
+
language="json",
|
| 106 |
+
value="{}"
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
with gr.Row():
|
| 110 |
+
execute_skill_btn = gr.Button("Execute", variant="primary")
|
| 111 |
+
dry_run_btn = gr.Button("Dry Run", variant="secondary")
|
| 112 |
+
|
| 113 |
+
skill_result = gr.TextArea(
|
| 114 |
+
label="Execution Result",
|
| 115 |
+
lines=8,
|
| 116 |
+
interactive=False
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
gr.Markdown("---")
|
| 120 |
+
|
| 121 |
+
# Agents Section
|
| 122 |
+
gr.Markdown("## Claude Agents")
|
| 123 |
+
gr.Markdown("Specialized AI agents for complex tasks. Activate an agent to see its capabilities.")
|
| 124 |
+
|
| 125 |
+
with gr.Row():
|
| 126 |
+
agent_dropdown = gr.Dropdown(
|
| 127 |
+
label="Select Agent",
|
| 128 |
+
choices=[a["name"] for a in AVAILABLE_AGENTS],
|
| 129 |
+
value=AVAILABLE_AGENTS[0]["name"] if AVAILABLE_AGENTS else None
|
| 130 |
+
)
|
| 131 |
+
activate_agent_btn = gr.Button("View Agent Details", variant="secondary")
|
| 132 |
+
|
| 133 |
+
agent_info = gr.Markdown("")
|
| 134 |
+
|
| 135 |
+
with gr.Accordion("Agent Descriptions", open=False):
|
| 136 |
+
agents_table = gr.Dataframe(
|
| 137 |
+
headers=["Agent", "Description"],
|
| 138 |
+
datatype=["str", "str"],
|
| 139 |
+
value=[[a["name"], a["description"]] for a in AVAILABLE_AGENTS],
|
| 140 |
+
interactive=False
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
# Event Handlers
|
| 144 |
+
def handle_refresh_skills(search: str = "") -> List[List]:
|
| 145 |
+
"""Refresh skills list"""
|
| 146 |
+
try:
|
| 147 |
+
if get_connection is None:
|
| 148 |
+
return [["N/A", "Module not available", ""]]
|
| 149 |
+
|
| 150 |
+
conn = get_connection()
|
| 151 |
+
if not conn.is_connected:
|
| 152 |
+
return [["N/A", "Not connected", ""]]
|
| 153 |
+
|
| 154 |
+
skills = conn.list_skills()
|
| 155 |
+
if not skills:
|
| 156 |
+
return [["No skills found", "", ""]]
|
| 157 |
+
|
| 158 |
+
rows = []
|
| 159 |
+
for skill in skills:
|
| 160 |
+
name = skill.get("name", "Unknown")
|
| 161 |
+
desc = skill.get("description", "")[:50] + "..." if len(skill.get("description", "")) > 50 else skill.get("description", "")
|
| 162 |
+
version = skill.get("version", "1.0.0")
|
| 163 |
+
|
| 164 |
+
if search and search.lower() not in name.lower() and search.lower() not in desc.lower():
|
| 165 |
+
continue
|
| 166 |
+
|
| 167 |
+
rows.append([name, desc, version])
|
| 168 |
+
|
| 169 |
+
return rows if rows else [["No matching skills", "", ""]]
|
| 170 |
+
except Exception as e:
|
| 171 |
+
logger.error(f"Skills list error: {e}")
|
| 172 |
+
return [[f"Error: {str(e)}", "", ""]]
|
| 173 |
+
|
| 174 |
+
def handle_select_skill(evt: gr.SelectData, data) -> tuple:
|
| 175 |
+
"""Handle skill selection"""
|
| 176 |
+
try:
|
| 177 |
+
if evt.index is not None and len(evt.index) >= 1:
|
| 178 |
+
row_idx = evt.index[0]
|
| 179 |
+
if data and len(data) > row_idx:
|
| 180 |
+
skill_name = data[row_idx][0]
|
| 181 |
+
|
| 182 |
+
# Get full skill info
|
| 183 |
+
if get_connection:
|
| 184 |
+
conn = get_connection()
|
| 185 |
+
if conn.is_connected:
|
| 186 |
+
# Try to get detailed skill info
|
| 187 |
+
result = conn.send_command("load_skill", skill_name=skill_name)
|
| 188 |
+
if result and result.get("success"):
|
| 189 |
+
skill_data = result.get("skill", {})
|
| 190 |
+
return skill_name, skill_data, "{}"
|
| 191 |
+
|
| 192 |
+
return skill_name, {"name": skill_name}, "{}"
|
| 193 |
+
except Exception as e:
|
| 194 |
+
logger.error(f"Skill selection error: {e}")
|
| 195 |
+
|
| 196 |
+
return "", {}, "{}"
|
| 197 |
+
|
| 198 |
+
def handle_execute_skill(skill_name: str, params_json: str, dry_run: bool = False) -> str:
|
| 199 |
+
"""Execute a skill"""
|
| 200 |
+
if not skill_name:
|
| 201 |
+
return "Select a skill first"
|
| 202 |
+
|
| 203 |
+
try:
|
| 204 |
+
params = json.loads(params_json) if params_json.strip() else {}
|
| 205 |
+
except json.JSONDecodeError as e:
|
| 206 |
+
return f"Invalid JSON parameters: {e}"
|
| 207 |
+
|
| 208 |
+
try:
|
| 209 |
+
if get_connection is None:
|
| 210 |
+
return "Module not available"
|
| 211 |
+
|
| 212 |
+
conn = get_connection()
|
| 213 |
+
if not conn.is_connected:
|
| 214 |
+
return "Not connected"
|
| 215 |
+
|
| 216 |
+
if dry_run:
|
| 217 |
+
return f"DRY RUN - Would execute skill '{skill_name}' with params:\n{json.dumps(params, indent=2)}"
|
| 218 |
+
|
| 219 |
+
result = conn.execute_skill(skill_name, params)
|
| 220 |
+
if result:
|
| 221 |
+
if result.get("success"):
|
| 222 |
+
return f"Success!\n\n{json.dumps(result.get('result', {}), indent=2)}"
|
| 223 |
+
return f"Failed: {result.get('error', 'Unknown error')}"
|
| 224 |
+
return "No response from server"
|
| 225 |
+
except Exception as e:
|
| 226 |
+
return f"Error: {str(e)}"
|
| 227 |
+
|
| 228 |
+
def handle_view_agent(agent_name: str) -> str:
|
| 229 |
+
"""View agent details"""
|
| 230 |
+
agent = next((a for a in AVAILABLE_AGENTS if a["name"] == agent_name), None)
|
| 231 |
+
if not agent:
|
| 232 |
+
return "Agent not found"
|
| 233 |
+
|
| 234 |
+
return f"""
|
| 235 |
+
### {agent['name']}
|
| 236 |
+
|
| 237 |
+
**Description:** {agent['description']}
|
| 238 |
+
|
| 239 |
+
**Configuration File:** `.claude/agents/{agent['file']}`
|
| 240 |
+
|
| 241 |
+
**How to Use:**
|
| 242 |
+
1. Open Claude Code in this project
|
| 243 |
+
2. Ask Claude to activate the {agent['name']} agent
|
| 244 |
+
3. Provide your task description
|
| 245 |
+
4. The agent will coordinate with UMCP tools to complete the task
|
| 246 |
+
|
| 247 |
+
**Example Prompts:**
|
| 248 |
+
- "Activate the {agent['name']} to help me..."
|
| 249 |
+
- "Use the {agent['name']} agent for..."
|
| 250 |
+
"""
|
| 251 |
+
|
| 252 |
+
# Wire up event handlers
|
| 253 |
+
refresh_skills_btn.click(
|
| 254 |
+
fn=handle_refresh_skills,
|
| 255 |
+
inputs=[skill_search],
|
| 256 |
+
outputs=[skills_list]
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
skill_search.change(
|
| 260 |
+
fn=handle_refresh_skills,
|
| 261 |
+
inputs=[skill_search],
|
| 262 |
+
outputs=[skills_list]
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
skills_list.select(
|
| 266 |
+
fn=handle_select_skill,
|
| 267 |
+
inputs=[skills_list],
|
| 268 |
+
outputs=[selected_skill, skill_info, skill_params]
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
execute_skill_btn.click(
|
| 272 |
+
fn=lambda n, p: handle_execute_skill(n, p, False),
|
| 273 |
+
inputs=[selected_skill, skill_params],
|
| 274 |
+
outputs=[skill_result]
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
dry_run_btn.click(
|
| 278 |
+
fn=lambda n, p: handle_execute_skill(n, p, True),
|
| 279 |
+
inputs=[selected_skill, skill_params],
|
| 280 |
+
outputs=[skill_result]
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
activate_agent_btn.click(
|
| 284 |
+
fn=handle_view_agent,
|
| 285 |
+
inputs=[agent_dropdown],
|
| 286 |
+
outputs=[agent_info]
|
| 287 |
+
)
|
| 288 |
+
|
| 289 |
+
return {
|
| 290 |
+
"skills_list": skills_list,
|
| 291 |
+
"selected_skill": selected_skill,
|
| 292 |
+
"skill_info": skill_info,
|
| 293 |
+
"skill_result": skill_result,
|
| 294 |
+
"agent_dropdown": agent_dropdown,
|
| 295 |
+
"agent_info": agent_info,
|
| 296 |
+
}
|
tabs/workflow_builder.py
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Kariana Unified UI - Workflow Builder Tab
|
| 3 |
+
Form-based workflow creation and execution
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import gradio as gr
|
| 7 |
+
import logging
|
| 8 |
+
from typing import Dict, List, Optional
|
| 9 |
+
import json
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
try:
|
| 15 |
+
from ..core.connection import get_connection
|
| 16 |
+
from ..core.session import get_session_manager
|
| 17 |
+
except ImportError:
|
| 18 |
+
get_connection = None
|
| 19 |
+
get_session_manager = None
|
| 20 |
+
|
| 21 |
+
# Available actions for workflow steps
|
| 22 |
+
WORKFLOW_ACTIONS = {
|
| 23 |
+
"spawn_actor": {
|
| 24 |
+
"description": "Spawn a new actor in the level",
|
| 25 |
+
"params": ["actor_type", "name", "location_x", "location_y", "location_z"]
|
| 26 |
+
},
|
| 27 |
+
"delete_actor": {
|
| 28 |
+
"description": "Delete an actor by name",
|
| 29 |
+
"params": ["actor_name"]
|
| 30 |
+
},
|
| 31 |
+
"set_actor_transform": {
|
| 32 |
+
"description": "Set actor location, rotation, scale",
|
| 33 |
+
"params": ["actor_name", "location_x", "location_y", "location_z", "rotation_x", "rotation_y", "rotation_z", "scale_x", "scale_y", "scale_z"]
|
| 34 |
+
},
|
| 35 |
+
"execute_python": {
|
| 36 |
+
"description": "Execute Python code in Unreal",
|
| 37 |
+
"params": ["code"]
|
| 38 |
+
},
|
| 39 |
+
"console_command": {
|
| 40 |
+
"description": "Execute Unreal console command",
|
| 41 |
+
"params": ["command"]
|
| 42 |
+
},
|
| 43 |
+
"capture_screenshot": {
|
| 44 |
+
"description": "Capture viewport screenshot",
|
| 45 |
+
"params": ["filename"]
|
| 46 |
+
},
|
| 47 |
+
"execute_skill": {
|
| 48 |
+
"description": "Execute a skill",
|
| 49 |
+
"params": ["skill_name", "params"]
|
| 50 |
+
},
|
| 51 |
+
"wait": {
|
| 52 |
+
"description": "Wait for specified seconds",
|
| 53 |
+
"params": ["seconds"]
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def create_workflow_builder_tab() -> Dict:
|
| 59 |
+
"""Create the Workflow Builder tab"""
|
| 60 |
+
|
| 61 |
+
with gr.Column():
|
| 62 |
+
gr.Markdown("## Workflow Builder")
|
| 63 |
+
gr.Markdown("Create multi-step workflows using a form-based interface.")
|
| 64 |
+
|
| 65 |
+
with gr.Row():
|
| 66 |
+
# Left Panel: Workflow Definition
|
| 67 |
+
with gr.Column(scale=2):
|
| 68 |
+
gr.Markdown("### Workflow Definition")
|
| 69 |
+
|
| 70 |
+
workflow_name = gr.Textbox(
|
| 71 |
+
label="Workflow Name",
|
| 72 |
+
placeholder="my_workflow"
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
workflow_description = gr.Textbox(
|
| 76 |
+
label="Description",
|
| 77 |
+
placeholder="What this workflow does...",
|
| 78 |
+
lines=2
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
gr.Markdown("---")
|
| 82 |
+
gr.Markdown("### Steps")
|
| 83 |
+
|
| 84 |
+
# Step Builder
|
| 85 |
+
with gr.Group():
|
| 86 |
+
gr.Markdown("**Add Step**")
|
| 87 |
+
|
| 88 |
+
step_type = gr.Dropdown(
|
| 89 |
+
label="Step Type",
|
| 90 |
+
choices=["Action", "Condition"],
|
| 91 |
+
value="Action"
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
action_dropdown = gr.Dropdown(
|
| 95 |
+
label="Action",
|
| 96 |
+
choices=list(WORKFLOW_ACTIONS.keys()),
|
| 97 |
+
value="spawn_actor"
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
action_description = gr.Markdown("*Select an action to see description*")
|
| 101 |
+
|
| 102 |
+
# Dynamic parameter inputs
|
| 103 |
+
param_1 = gr.Textbox(label="Parameter 1", visible=True)
|
| 104 |
+
param_2 = gr.Textbox(label="Parameter 2", visible=True)
|
| 105 |
+
param_3 = gr.Textbox(label="Parameter 3", visible=True)
|
| 106 |
+
param_4 = gr.Textbox(label="Parameter 4", visible=False)
|
| 107 |
+
param_5 = gr.Textbox(label="Parameter 5", visible=False)
|
| 108 |
+
|
| 109 |
+
with gr.Row():
|
| 110 |
+
add_step_btn = gr.Button("Add Step", variant="primary")
|
| 111 |
+
clear_form_btn = gr.Button("Clear Form")
|
| 112 |
+
|
| 113 |
+
# Steps List (JSON representation)
|
| 114 |
+
steps_json = gr.Code(
|
| 115 |
+
label="Workflow Steps (JSON)",
|
| 116 |
+
language="json",
|
| 117 |
+
value="[]",
|
| 118 |
+
lines=10
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
with gr.Row():
|
| 122 |
+
remove_last_step_btn = gr.Button("Remove Last Step", size="sm")
|
| 123 |
+
clear_all_steps_btn = gr.Button("Clear All Steps", size="sm", variant="stop")
|
| 124 |
+
|
| 125 |
+
# Right Panel: Execution & Saved Workflows
|
| 126 |
+
with gr.Column(scale=1):
|
| 127 |
+
gr.Markdown("### Execution")
|
| 128 |
+
|
| 129 |
+
with gr.Row():
|
| 130 |
+
execute_workflow_btn = gr.Button("Execute Workflow", variant="primary")
|
| 131 |
+
stop_workflow_btn = gr.Button("Stop", variant="stop")
|
| 132 |
+
|
| 133 |
+
execution_log = gr.TextArea(
|
| 134 |
+
label="Execution Log",
|
| 135 |
+
lines=12,
|
| 136 |
+
interactive=False
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
execution_progress = gr.Progress()
|
| 140 |
+
|
| 141 |
+
gr.Markdown("---")
|
| 142 |
+
gr.Markdown("### Save & Load")
|
| 143 |
+
|
| 144 |
+
with gr.Row():
|
| 145 |
+
save_workflow_btn = gr.Button("Save Workflow", size="sm")
|
| 146 |
+
load_workflow_dropdown = gr.Dropdown(
|
| 147 |
+
label="Load Workflow",
|
| 148 |
+
choices=[],
|
| 149 |
+
scale=2
|
| 150 |
+
)
|
| 151 |
+
load_btn = gr.Button("Load", size="sm")
|
| 152 |
+
|
| 153 |
+
with gr.Accordion("Saved Workflows", open=False):
|
| 154 |
+
saved_workflows_list = gr.Dataframe(
|
| 155 |
+
headers=["Name", "Steps", "Created"],
|
| 156 |
+
datatype=["str", "number", "str"],
|
| 157 |
+
interactive=False
|
| 158 |
+
)
|
| 159 |
+
refresh_saved_btn = gr.Button("Refresh List", size="sm")
|
| 160 |
+
delete_workflow_btn = gr.Button("Delete Selected", size="sm", variant="stop")
|
| 161 |
+
|
| 162 |
+
gr.Markdown("---")
|
| 163 |
+
|
| 164 |
+
# Templates Section
|
| 165 |
+
with gr.Accordion("Workflow Templates", open=False):
|
| 166 |
+
gr.Markdown("Quick-start templates for common workflows:")
|
| 167 |
+
|
| 168 |
+
with gr.Row():
|
| 169 |
+
template_scatter = gr.Button("Scatter Objects", size="sm")
|
| 170 |
+
template_lighting = gr.Button("3-Point Lighting", size="sm")
|
| 171 |
+
template_cleanup = gr.Button("Scene Cleanup", size="sm")
|
| 172 |
+
template_screenshot = gr.Button("Screenshot Sequence", size="sm")
|
| 173 |
+
|
| 174 |
+
# Event Handlers
|
| 175 |
+
def update_action_params(action: str) -> tuple:
|
| 176 |
+
"""Update parameter fields based on selected action"""
|
| 177 |
+
if action not in WORKFLOW_ACTIONS:
|
| 178 |
+
return gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), ""
|
| 179 |
+
|
| 180 |
+
action_info = WORKFLOW_ACTIONS[action]
|
| 181 |
+
params = action_info["params"]
|
| 182 |
+
desc = f"*{action_info['description']}*"
|
| 183 |
+
|
| 184 |
+
# Show/hide and label parameter fields
|
| 185 |
+
updates = []
|
| 186 |
+
for i in range(5):
|
| 187 |
+
if i < len(params):
|
| 188 |
+
updates.append(gr.update(visible=True, label=params[i], value=""))
|
| 189 |
+
else:
|
| 190 |
+
updates.append(gr.update(visible=False, value=""))
|
| 191 |
+
|
| 192 |
+
return *updates, desc
|
| 193 |
+
|
| 194 |
+
def add_step(action: str, p1: str, p2: str, p3: str, p4: str, p5: str, current_steps: str) -> str:
|
| 195 |
+
"""Add a step to the workflow"""
|
| 196 |
+
try:
|
| 197 |
+
steps = json.loads(current_steps) if current_steps else []
|
| 198 |
+
except:
|
| 199 |
+
steps = []
|
| 200 |
+
|
| 201 |
+
action_info = WORKFLOW_ACTIONS.get(action, {"params": []})
|
| 202 |
+
params = action_info["params"]
|
| 203 |
+
|
| 204 |
+
# Build step with filled parameters
|
| 205 |
+
step = {"action": action, "params": {}}
|
| 206 |
+
param_values = [p1, p2, p3, p4, p5]
|
| 207 |
+
|
| 208 |
+
for i, param_name in enumerate(params):
|
| 209 |
+
if i < len(param_values) and param_values[i]:
|
| 210 |
+
step["params"][param_name] = param_values[i]
|
| 211 |
+
|
| 212 |
+
steps.append(step)
|
| 213 |
+
return json.dumps(steps, indent=2)
|
| 214 |
+
|
| 215 |
+
def remove_last_step(current_steps: str) -> str:
|
| 216 |
+
"""Remove the last step"""
|
| 217 |
+
try:
|
| 218 |
+
steps = json.loads(current_steps) if current_steps else []
|
| 219 |
+
if steps:
|
| 220 |
+
steps.pop()
|
| 221 |
+
return json.dumps(steps, indent=2)
|
| 222 |
+
except:
|
| 223 |
+
return "[]"
|
| 224 |
+
|
| 225 |
+
def execute_workflow(name: str, desc: str, steps_json: str) -> str:
|
| 226 |
+
"""Execute the workflow"""
|
| 227 |
+
try:
|
| 228 |
+
steps = json.loads(steps_json) if steps_json else []
|
| 229 |
+
except json.JSONDecodeError:
|
| 230 |
+
return "Invalid workflow JSON"
|
| 231 |
+
|
| 232 |
+
if not steps:
|
| 233 |
+
return "No steps to execute"
|
| 234 |
+
|
| 235 |
+
log_lines = [f"Executing workflow: {name or 'Unnamed'}"]
|
| 236 |
+
log_lines.append(f"Description: {desc or 'No description'}")
|
| 237 |
+
log_lines.append(f"Total steps: {len(steps)}")
|
| 238 |
+
log_lines.append("-" * 40)
|
| 239 |
+
|
| 240 |
+
if get_connection is None:
|
| 241 |
+
return "\n".join(log_lines + ["ERROR: Connection module not available"])
|
| 242 |
+
|
| 243 |
+
conn = get_connection()
|
| 244 |
+
if not conn.is_connected:
|
| 245 |
+
return "\n".join(log_lines + ["ERROR: Not connected to Unreal"])
|
| 246 |
+
|
| 247 |
+
for i, step in enumerate(steps):
|
| 248 |
+
action = step.get("action", "unknown")
|
| 249 |
+
params = step.get("params", {})
|
| 250 |
+
|
| 251 |
+
log_lines.append(f"\n[Step {i+1}/{len(steps)}] {action}")
|
| 252 |
+
|
| 253 |
+
try:
|
| 254 |
+
if action == "wait":
|
| 255 |
+
import time
|
| 256 |
+
seconds = float(params.get("seconds", 1))
|
| 257 |
+
time.sleep(seconds)
|
| 258 |
+
log_lines.append(f" Waited {seconds}s")
|
| 259 |
+
elif action == "spawn_actor":
|
| 260 |
+
loc = [
|
| 261 |
+
float(params.get("location_x", 0)),
|
| 262 |
+
float(params.get("location_y", 0)),
|
| 263 |
+
float(params.get("location_z", 0))
|
| 264 |
+
]
|
| 265 |
+
result = conn.spawn_actor(
|
| 266 |
+
params.get("actor_type", "Cube"),
|
| 267 |
+
params.get("name", f"WorkflowActor_{i}"),
|
| 268 |
+
location=loc
|
| 269 |
+
)
|
| 270 |
+
log_lines.append(f" Result: {result.get('success', False)}")
|
| 271 |
+
elif action == "delete_actor":
|
| 272 |
+
result = conn.delete_actor(params.get("actor_name", ""))
|
| 273 |
+
log_lines.append(f" Result: {result.get('success', False)}")
|
| 274 |
+
elif action == "execute_python":
|
| 275 |
+
result = conn.execute_python(params.get("code", ""))
|
| 276 |
+
log_lines.append(f" Result: {result.get('success', False)}")
|
| 277 |
+
elif action == "console_command":
|
| 278 |
+
result = conn.console_command(params.get("command", ""))
|
| 279 |
+
log_lines.append(f" Result: {result.get('success', False)}")
|
| 280 |
+
elif action == "capture_screenshot":
|
| 281 |
+
result = conn.capture_screenshot(params.get("filename"))
|
| 282 |
+
log_lines.append(f" Result: {result.get('success', False)}")
|
| 283 |
+
elif action == "execute_skill":
|
| 284 |
+
skill_params = json.loads(params.get("params", "{}"))
|
| 285 |
+
result = conn.execute_skill(params.get("skill_name", ""), skill_params)
|
| 286 |
+
log_lines.append(f" Result: {result.get('success', False)}")
|
| 287 |
+
else:
|
| 288 |
+
log_lines.append(f" Unknown action: {action}")
|
| 289 |
+
|
| 290 |
+
except Exception as e:
|
| 291 |
+
log_lines.append(f" ERROR: {str(e)}")
|
| 292 |
+
|
| 293 |
+
log_lines.append("\n" + "-" * 40)
|
| 294 |
+
log_lines.append("Workflow completed!")
|
| 295 |
+
return "\n".join(log_lines)
|
| 296 |
+
|
| 297 |
+
def save_workflow(name: str, desc: str, steps_json: str) -> tuple:
|
| 298 |
+
"""Save workflow to session"""
|
| 299 |
+
if not name:
|
| 300 |
+
return "Enter a workflow name", gr.update()
|
| 301 |
+
|
| 302 |
+
try:
|
| 303 |
+
steps = json.loads(steps_json) if steps_json else []
|
| 304 |
+
except:
|
| 305 |
+
return "Invalid workflow JSON", gr.update()
|
| 306 |
+
|
| 307 |
+
workflow = {
|
| 308 |
+
"name": name,
|
| 309 |
+
"description": desc,
|
| 310 |
+
"steps": steps,
|
| 311 |
+
"created": datetime.now().isoformat()
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
if get_session_manager:
|
| 315 |
+
session_mgr = get_session_manager()
|
| 316 |
+
session_mgr.save_workflow(workflow)
|
| 317 |
+
|
| 318 |
+
return f"Saved workflow: {name}", gr.update(choices=get_saved_workflow_names())
|
| 319 |
+
|
| 320 |
+
def get_saved_workflow_names() -> List[str]:
|
| 321 |
+
"""Get list of saved workflow names"""
|
| 322 |
+
if get_session_manager:
|
| 323 |
+
session_mgr = get_session_manager()
|
| 324 |
+
workflows = session_mgr.get_workflows()
|
| 325 |
+
return [w.get("name", "unnamed") for w in workflows]
|
| 326 |
+
return []
|
| 327 |
+
|
| 328 |
+
def load_workflow(name: str) -> tuple:
|
| 329 |
+
"""Load a saved workflow"""
|
| 330 |
+
if not name or not get_session_manager:
|
| 331 |
+
return "", "", "[]"
|
| 332 |
+
|
| 333 |
+
session_mgr = get_session_manager()
|
| 334 |
+
workflows = session_mgr.get_workflows()
|
| 335 |
+
|
| 336 |
+
for w in workflows:
|
| 337 |
+
if w.get("name") == name:
|
| 338 |
+
return w.get("name", ""), w.get("description", ""), json.dumps(w.get("steps", []), indent=2)
|
| 339 |
+
|
| 340 |
+
return "", "", "[]"
|
| 341 |
+
|
| 342 |
+
def get_saved_workflows_data() -> List[List]:
|
| 343 |
+
"""Get saved workflows for display"""
|
| 344 |
+
if not get_session_manager:
|
| 345 |
+
return []
|
| 346 |
+
|
| 347 |
+
session_mgr = get_session_manager()
|
| 348 |
+
workflows = session_mgr.get_workflows()
|
| 349 |
+
|
| 350 |
+
return [
|
| 351 |
+
[w.get("name", ""), len(w.get("steps", [])), w.get("created", "")[:10]]
|
| 352 |
+
for w in workflows
|
| 353 |
+
]
|
| 354 |
+
|
| 355 |
+
def apply_template(template_name: str) -> tuple:
|
| 356 |
+
"""Apply a workflow template"""
|
| 357 |
+
templates = {
|
| 358 |
+
"scatter": {
|
| 359 |
+
"name": "scatter_objects",
|
| 360 |
+
"description": "Scatter multiple objects in the scene",
|
| 361 |
+
"steps": [
|
| 362 |
+
{"action": "spawn_actor", "params": {"actor_type": "Cube", "name": "ScatterCube_1", "location_x": "0", "location_y": "0", "location_z": "0"}},
|
| 363 |
+
{"action": "spawn_actor", "params": {"actor_type": "Cube", "name": "ScatterCube_2", "location_x": "200", "location_y": "0", "location_z": "0"}},
|
| 364 |
+
{"action": "spawn_actor", "params": {"actor_type": "Cube", "name": "ScatterCube_3", "location_x": "0", "location_y": "200", "location_z": "0"}},
|
| 365 |
+
]
|
| 366 |
+
},
|
| 367 |
+
"lighting": {
|
| 368 |
+
"name": "three_point_lighting",
|
| 369 |
+
"description": "Create a 3-point lighting setup",
|
| 370 |
+
"steps": [
|
| 371 |
+
{"action": "spawn_actor", "params": {"actor_type": "PointLight", "name": "KeyLight", "location_x": "300", "location_y": "-300", "location_z": "400"}},
|
| 372 |
+
{"action": "spawn_actor", "params": {"actor_type": "PointLight", "name": "FillLight", "location_x": "-300", "location_y": "-200", "location_z": "300"}},
|
| 373 |
+
{"action": "spawn_actor", "params": {"actor_type": "PointLight", "name": "BackLight", "location_x": "0", "location_y": "400", "location_z": "350"}},
|
| 374 |
+
]
|
| 375 |
+
},
|
| 376 |
+
"cleanup": {
|
| 377 |
+
"name": "scene_cleanup",
|
| 378 |
+
"description": "Clean up scene by removing test actors",
|
| 379 |
+
"steps": [
|
| 380 |
+
{"action": "console_command", "params": {"command": "stat none"}},
|
| 381 |
+
{"action": "execute_python", "params": {"code": "unreal.log('Scene cleanup started')"}},
|
| 382 |
+
]
|
| 383 |
+
},
|
| 384 |
+
"screenshot": {
|
| 385 |
+
"name": "screenshot_sequence",
|
| 386 |
+
"description": "Capture multiple screenshots",
|
| 387 |
+
"steps": [
|
| 388 |
+
{"action": "capture_screenshot", "params": {"filename": "shot_1.png"}},
|
| 389 |
+
{"action": "wait", "params": {"seconds": "1"}},
|
| 390 |
+
{"action": "capture_screenshot", "params": {"filename": "shot_2.png"}},
|
| 391 |
+
]
|
| 392 |
+
}
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
template = templates.get(template_name, templates["scatter"])
|
| 396 |
+
return template["name"], template["description"], json.dumps(template["steps"], indent=2)
|
| 397 |
+
|
| 398 |
+
# Wire up event handlers
|
| 399 |
+
action_dropdown.change(
|
| 400 |
+
fn=update_action_params,
|
| 401 |
+
inputs=[action_dropdown],
|
| 402 |
+
outputs=[param_1, param_2, param_3, param_4, param_5, action_description]
|
| 403 |
+
)
|
| 404 |
+
|
| 405 |
+
add_step_btn.click(
|
| 406 |
+
fn=add_step,
|
| 407 |
+
inputs=[action_dropdown, param_1, param_2, param_3, param_4, param_5, steps_json],
|
| 408 |
+
outputs=[steps_json]
|
| 409 |
+
)
|
| 410 |
+
|
| 411 |
+
clear_form_btn.click(
|
| 412 |
+
fn=lambda: ("", "", "", "", ""),
|
| 413 |
+
outputs=[param_1, param_2, param_3, param_4, param_5]
|
| 414 |
+
)
|
| 415 |
+
|
| 416 |
+
remove_last_step_btn.click(
|
| 417 |
+
fn=remove_last_step,
|
| 418 |
+
inputs=[steps_json],
|
| 419 |
+
outputs=[steps_json]
|
| 420 |
+
)
|
| 421 |
+
|
| 422 |
+
clear_all_steps_btn.click(
|
| 423 |
+
fn=lambda: "[]",
|
| 424 |
+
outputs=[steps_json]
|
| 425 |
+
)
|
| 426 |
+
|
| 427 |
+
execute_workflow_btn.click(
|
| 428 |
+
fn=execute_workflow,
|
| 429 |
+
inputs=[workflow_name, workflow_description, steps_json],
|
| 430 |
+
outputs=[execution_log]
|
| 431 |
+
)
|
| 432 |
+
|
| 433 |
+
save_workflow_btn.click(
|
| 434 |
+
fn=save_workflow,
|
| 435 |
+
inputs=[workflow_name, workflow_description, steps_json],
|
| 436 |
+
outputs=[execution_log, load_workflow_dropdown]
|
| 437 |
+
)
|
| 438 |
+
|
| 439 |
+
load_btn.click(
|
| 440 |
+
fn=load_workflow,
|
| 441 |
+
inputs=[load_workflow_dropdown],
|
| 442 |
+
outputs=[workflow_name, workflow_description, steps_json]
|
| 443 |
+
)
|
| 444 |
+
|
| 445 |
+
refresh_saved_btn.click(
|
| 446 |
+
fn=get_saved_workflows_data,
|
| 447 |
+
outputs=[saved_workflows_list]
|
| 448 |
+
)
|
| 449 |
+
|
| 450 |
+
# Template buttons
|
| 451 |
+
template_scatter.click(fn=lambda: apply_template("scatter"), outputs=[workflow_name, workflow_description, steps_json])
|
| 452 |
+
template_lighting.click(fn=lambda: apply_template("lighting"), outputs=[workflow_name, workflow_description, steps_json])
|
| 453 |
+
template_cleanup.click(fn=lambda: apply_template("cleanup"), outputs=[workflow_name, workflow_description, steps_json])
|
| 454 |
+
template_screenshot.click(fn=lambda: apply_template("screenshot"), outputs=[workflow_name, workflow_description, steps_json])
|
| 455 |
+
|
| 456 |
+
return {
|
| 457 |
+
"workflow_name": workflow_name,
|
| 458 |
+
"steps_json": steps_json,
|
| 459 |
+
"execution_log": execution_log,
|
| 460 |
+
}
|
utils/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Kariana Unified UI - Utils Module
|
| 2 |
+
from .socket_client import SocketClient
|
| 3 |
+
from .discovery import InstanceDiscovery
|
| 4 |
+
|
| 5 |
+
__all__ = ['SocketClient', 'InstanceDiscovery']
|
utils/discovery.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Kariana Unified UI - Instance Discovery
|
| 3 |
+
Discovers running KarianaUMCP instances on the network
|
| 4 |
+
"""
|
| 5 |
+
import asyncio
|
| 6 |
+
import json
|
| 7 |
+
import logging
|
| 8 |
+
from dataclasses import dataclass
|
| 9 |
+
from typing import List, Optional
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass
|
| 15 |
+
class UnrealInstance:
|
| 16 |
+
"""Represents a discovered Unreal Engine instance"""
|
| 17 |
+
host: str
|
| 18 |
+
port: int
|
| 19 |
+
instance_id: Optional[str] = None
|
| 20 |
+
project_name: Optional[str] = None
|
| 21 |
+
version: Optional[str] = None
|
| 22 |
+
uptime: Optional[float] = None
|
| 23 |
+
authenticated: bool = False
|
| 24 |
+
|
| 25 |
+
def to_dict(self) -> dict:
|
| 26 |
+
return {
|
| 27 |
+
"host": self.host,
|
| 28 |
+
"port": self.port,
|
| 29 |
+
"instance_id": self.instance_id,
|
| 30 |
+
"project_name": self.project_name,
|
| 31 |
+
"version": self.version,
|
| 32 |
+
"uptime": self.uptime,
|
| 33 |
+
"authenticated": self.authenticated,
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class InstanceDiscovery:
|
| 38 |
+
"""Discovers running KarianaUMCP socket server instances"""
|
| 39 |
+
|
| 40 |
+
def __init__(
|
| 41 |
+
self,
|
| 42 |
+
host: str = "localhost",
|
| 43 |
+
port_range: tuple = (9877, 9887),
|
| 44 |
+
timeout: float = 1.0
|
| 45 |
+
):
|
| 46 |
+
self.host = host
|
| 47 |
+
self.port_range = port_range
|
| 48 |
+
self.timeout = timeout
|
| 49 |
+
|
| 50 |
+
async def probe_port(self, port: int) -> Optional[UnrealInstance]:
|
| 51 |
+
"""Probe a single port for KarianaUMCP instance"""
|
| 52 |
+
try:
|
| 53 |
+
reader, writer = await asyncio.wait_for(
|
| 54 |
+
asyncio.open_connection(self.host, port),
|
| 55 |
+
timeout=self.timeout
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
# Send get_server_info command
|
| 59 |
+
command = json.dumps({"type": "get_server_info"}) + "\n"
|
| 60 |
+
writer.write(command.encode())
|
| 61 |
+
await writer.drain()
|
| 62 |
+
|
| 63 |
+
# Read response
|
| 64 |
+
response_data = await asyncio.wait_for(
|
| 65 |
+
reader.readline(),
|
| 66 |
+
timeout=self.timeout
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
writer.close()
|
| 70 |
+
await writer.wait_closed()
|
| 71 |
+
|
| 72 |
+
if response_data:
|
| 73 |
+
response = json.loads(response_data.decode().strip())
|
| 74 |
+
if response.get("status") == "ok" or response.get("success"):
|
| 75 |
+
return UnrealInstance(
|
| 76 |
+
host=self.host,
|
| 77 |
+
port=port,
|
| 78 |
+
instance_id=response.get("instance_id"),
|
| 79 |
+
project_name=response.get("project_name"),
|
| 80 |
+
version=response.get("version"),
|
| 81 |
+
uptime=response.get("uptime"),
|
| 82 |
+
)
|
| 83 |
+
except asyncio.TimeoutError:
|
| 84 |
+
pass
|
| 85 |
+
except ConnectionRefusedError:
|
| 86 |
+
pass
|
| 87 |
+
except json.JSONDecodeError:
|
| 88 |
+
pass
|
| 89 |
+
except Exception as e:
|
| 90 |
+
logger.debug(f"Probe error on port {port}: {e}")
|
| 91 |
+
|
| 92 |
+
return None
|
| 93 |
+
|
| 94 |
+
async def discover_instances(self) -> List[UnrealInstance]:
|
| 95 |
+
"""Discover all running instances in port range"""
|
| 96 |
+
tasks = [
|
| 97 |
+
self.probe_port(port)
|
| 98 |
+
for port in range(*self.port_range)
|
| 99 |
+
]
|
| 100 |
+
|
| 101 |
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
| 102 |
+
|
| 103 |
+
instances = []
|
| 104 |
+
for result in results:
|
| 105 |
+
if isinstance(result, UnrealInstance):
|
| 106 |
+
instances.append(result)
|
| 107 |
+
|
| 108 |
+
logger.info(f"Discovered {len(instances)} instance(s)")
|
| 109 |
+
return instances
|
| 110 |
+
|
| 111 |
+
def discover_instances_sync(self) -> List[UnrealInstance]:
|
| 112 |
+
"""Synchronous wrapper for discover_instances"""
|
| 113 |
+
try:
|
| 114 |
+
loop = asyncio.get_running_loop()
|
| 115 |
+
import concurrent.futures
|
| 116 |
+
with concurrent.futures.ThreadPoolExecutor() as pool:
|
| 117 |
+
future = pool.submit(asyncio.run, self.discover_instances())
|
| 118 |
+
return future.result(timeout=self.timeout * 10 + 1)
|
| 119 |
+
except RuntimeError:
|
| 120 |
+
return asyncio.run(self.discover_instances())
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def discover_instances(
|
| 124 |
+
host: str = "localhost",
|
| 125 |
+
port_range: tuple = (9877, 9887)
|
| 126 |
+
) -> List[UnrealInstance]:
|
| 127 |
+
"""Convenience function for instance discovery"""
|
| 128 |
+
discovery = InstanceDiscovery(host, port_range)
|
| 129 |
+
return discovery.discover_instances_sync()
|
utils/socket_client.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Kariana Unified UI - Async Socket Client
|
| 3 |
+
Handles communication with KarianaUMCP socket server
|
| 4 |
+
"""
|
| 5 |
+
import asyncio
|
| 6 |
+
import json
|
| 7 |
+
import logging
|
| 8 |
+
from typing import Any, Optional, Callable
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class SocketClient:
|
| 14 |
+
"""Async socket client for communicating with KarianaUMCP"""
|
| 15 |
+
|
| 16 |
+
def __init__(self, host: str = "localhost", port: int = 9877, timeout: float = 5.0):
|
| 17 |
+
self.host = host
|
| 18 |
+
self.port = port
|
| 19 |
+
self.timeout = timeout
|
| 20 |
+
self._reader: Optional[asyncio.StreamReader] = None
|
| 21 |
+
self._writer: Optional[asyncio.StreamWriter] = None
|
| 22 |
+
self._connected = False
|
| 23 |
+
self._lock = asyncio.Lock()
|
| 24 |
+
|
| 25 |
+
@property
|
| 26 |
+
def connected(self) -> bool:
|
| 27 |
+
return self._connected and self._writer is not None
|
| 28 |
+
|
| 29 |
+
async def connect(self) -> bool:
|
| 30 |
+
"""Establish connection to socket server"""
|
| 31 |
+
try:
|
| 32 |
+
self._reader, self._writer = await asyncio.wait_for(
|
| 33 |
+
asyncio.open_connection(self.host, self.port),
|
| 34 |
+
timeout=self.timeout
|
| 35 |
+
)
|
| 36 |
+
self._connected = True
|
| 37 |
+
logger.info(f"Connected to {self.host}:{self.port}")
|
| 38 |
+
return True
|
| 39 |
+
except asyncio.TimeoutError:
|
| 40 |
+
logger.warning(f"Connection timeout to {self.host}:{self.port}")
|
| 41 |
+
return False
|
| 42 |
+
except ConnectionRefusedError:
|
| 43 |
+
logger.warning(f"Connection refused at {self.host}:{self.port}")
|
| 44 |
+
return False
|
| 45 |
+
except Exception as e:
|
| 46 |
+
logger.error(f"Connection error: {e}")
|
| 47 |
+
return False
|
| 48 |
+
|
| 49 |
+
async def disconnect(self):
|
| 50 |
+
"""Close connection"""
|
| 51 |
+
if self._writer:
|
| 52 |
+
try:
|
| 53 |
+
self._writer.close()
|
| 54 |
+
await self._writer.wait_closed()
|
| 55 |
+
except Exception as e:
|
| 56 |
+
logger.error(f"Error closing connection: {e}")
|
| 57 |
+
finally:
|
| 58 |
+
self._writer = None
|
| 59 |
+
self._reader = None
|
| 60 |
+
self._connected = False
|
| 61 |
+
|
| 62 |
+
async def send_command(self, command: dict) -> Optional[dict]:
|
| 63 |
+
"""Send a command and receive response"""
|
| 64 |
+
async with self._lock:
|
| 65 |
+
if not self.connected:
|
| 66 |
+
if not await self.connect():
|
| 67 |
+
return None
|
| 68 |
+
|
| 69 |
+
try:
|
| 70 |
+
# Send command as JSON with newline delimiter
|
| 71 |
+
message = json.dumps(command) + "\n"
|
| 72 |
+
self._writer.write(message.encode())
|
| 73 |
+
await self._writer.drain()
|
| 74 |
+
|
| 75 |
+
# Read response (newline-delimited JSON)
|
| 76 |
+
response_data = await asyncio.wait_for(
|
| 77 |
+
self._reader.readline(),
|
| 78 |
+
timeout=self.timeout
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
if not response_data:
|
| 82 |
+
logger.warning("Empty response received")
|
| 83 |
+
return None
|
| 84 |
+
|
| 85 |
+
return json.loads(response_data.decode().strip())
|
| 86 |
+
|
| 87 |
+
except asyncio.TimeoutError:
|
| 88 |
+
logger.warning("Response timeout")
|
| 89 |
+
await self.disconnect()
|
| 90 |
+
return None
|
| 91 |
+
except json.JSONDecodeError as e:
|
| 92 |
+
logger.error(f"Invalid JSON response: {e}")
|
| 93 |
+
return None
|
| 94 |
+
except Exception as e:
|
| 95 |
+
logger.error(f"Command error: {e}")
|
| 96 |
+
await self.disconnect()
|
| 97 |
+
return None
|
| 98 |
+
|
| 99 |
+
async def ping(self) -> bool:
|
| 100 |
+
"""Test connection with ping command"""
|
| 101 |
+
response = await self.send_command({"type": "ping"})
|
| 102 |
+
return response is not None and response.get("status") == "ok"
|
| 103 |
+
|
| 104 |
+
async def subscribe(self, event_type: str, callback: Callable[[dict], None]):
|
| 105 |
+
"""Subscribe to real-time events (logs, etc.)"""
|
| 106 |
+
# Subscribe command
|
| 107 |
+
response = await self.send_command({
|
| 108 |
+
"type": f"subscribe_{event_type}"
|
| 109 |
+
})
|
| 110 |
+
|
| 111 |
+
if not response or not response.get("success"):
|
| 112 |
+
return False
|
| 113 |
+
|
| 114 |
+
# Start listening for events in background
|
| 115 |
+
asyncio.create_task(self._event_listener(event_type, callback))
|
| 116 |
+
return True
|
| 117 |
+
|
| 118 |
+
async def _event_listener(self, event_type: str, callback: Callable[[dict], None]):
|
| 119 |
+
"""Background listener for subscribed events"""
|
| 120 |
+
try:
|
| 121 |
+
while self.connected:
|
| 122 |
+
try:
|
| 123 |
+
data = await asyncio.wait_for(
|
| 124 |
+
self._reader.readline(),
|
| 125 |
+
timeout=1.0
|
| 126 |
+
)
|
| 127 |
+
if data:
|
| 128 |
+
event = json.loads(data.decode().strip())
|
| 129 |
+
if event.get("event_type") == event_type:
|
| 130 |
+
callback(event)
|
| 131 |
+
except asyncio.TimeoutError:
|
| 132 |
+
continue
|
| 133 |
+
except json.JSONDecodeError:
|
| 134 |
+
continue
|
| 135 |
+
except Exception as e:
|
| 136 |
+
logger.error(f"Event listener error: {e}")
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
class SyncSocketClient:
|
| 140 |
+
"""Synchronous wrapper for SocketClient (for Gradio callbacks)"""
|
| 141 |
+
|
| 142 |
+
def __init__(self, host: str = "localhost", port: int = 9877, timeout: float = 5.0):
|
| 143 |
+
self.host = host
|
| 144 |
+
self.port = port
|
| 145 |
+
self.timeout = timeout
|
| 146 |
+
self._client: Optional[SocketClient] = None
|
| 147 |
+
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
| 148 |
+
|
| 149 |
+
def _get_loop(self) -> asyncio.AbstractEventLoop:
|
| 150 |
+
"""Get or create event loop"""
|
| 151 |
+
try:
|
| 152 |
+
return asyncio.get_running_loop()
|
| 153 |
+
except RuntimeError:
|
| 154 |
+
if self._loop is None or self._loop.is_closed():
|
| 155 |
+
self._loop = asyncio.new_event_loop()
|
| 156 |
+
return self._loop
|
| 157 |
+
|
| 158 |
+
def _run(self, coro):
|
| 159 |
+
"""Run coroutine synchronously"""
|
| 160 |
+
loop = self._get_loop()
|
| 161 |
+
try:
|
| 162 |
+
return loop.run_until_complete(coro)
|
| 163 |
+
except RuntimeError:
|
| 164 |
+
# Already running in async context, use nest_asyncio pattern
|
| 165 |
+
import concurrent.futures
|
| 166 |
+
with concurrent.futures.ThreadPoolExecutor() as pool:
|
| 167 |
+
future = pool.submit(asyncio.run, coro)
|
| 168 |
+
return future.result(timeout=self.timeout + 1)
|
| 169 |
+
|
| 170 |
+
def connect(self) -> bool:
|
| 171 |
+
self._client = SocketClient(self.host, self.port, self.timeout)
|
| 172 |
+
return self._run(self._client.connect())
|
| 173 |
+
|
| 174 |
+
def disconnect(self):
|
| 175 |
+
if self._client:
|
| 176 |
+
self._run(self._client.disconnect())
|
| 177 |
+
|
| 178 |
+
def send_command(self, command: dict) -> Optional[dict]:
|
| 179 |
+
if not self._client:
|
| 180 |
+
self._client = SocketClient(self.host, self.port, self.timeout)
|
| 181 |
+
return self._run(self._client.send_command(command))
|
| 182 |
+
|
| 183 |
+
def ping(self) -> bool:
|
| 184 |
+
if not self._client:
|
| 185 |
+
self._client = SocketClient(self.host, self.port, self.timeout)
|
| 186 |
+
return self._run(self._client.ping())
|