barlowski commited on
Commit
22d587c
·
verified ·
1 Parent(s): 60d2bbe

Upload folder using huggingface_hub

Browse files
README.md CHANGED
@@ -1,561 +1,119 @@
1
- # KARiANA.ai MCP Tools for Unreal Engine
2
-
3
- [![Unreal Engine](https://img.shields.io/badge/Unreal%20Engine-5.6%2B-blue)](https://www.unrealengine.com/)
4
- [![MCP](https://img.shields.io/badge/MCP-Protocol-green)](https://modelcontextprotocol.io/)
5
- [![Docker](https://img.shields.io/badge/Docker-Ready-2496ED?logo=docker)](https://www.docker.com/)
6
- [![Documentation](https://img.shields.io/badge/Docs-GitHub%20Pages-4078c0)](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
- ## 🚀 Quick Start
31
-
32
- ### Windows
33
- ```powershell
34
- # Visual installer with progress tracking
35
- .\Install-MCPDocker-GUI.ps1
36
- ```
37
-
38
- ### Linux/Mac
39
- ```bash
40
- # Fully automated CLI installer
41
- chmod +x setup-docker.sh
42
- ./setup-docker.sh
43
- ```
44
-
45
- ### Manual Setup
46
- ```bash
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
- ## 📚 Documentation
77
-
78
- Visit our [**GitHub Pages Documentation**](https://kerinzeebart.github.io/MCPTest-with-UMCP.it) for comprehensive guides.
79
 
80
- ### Getting Started
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
- ### Development
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
- ### Operations
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
- ## 🛠️ Tool Categories
98
 
99
- ### External MCP Server (50+ Actions)
100
- Via Remote Control API (ports 30010/30020)
101
 
102
- | Category | Tools | Description |
103
- |----------|-------|-------------|
104
- | **Asset Management** | 4 | List, import, create, inspect assets |
105
- | **Actor Control** | 8 | Spawn, move, rotate, scale, delete actors |
106
- | **Editor Control** | 6 | PIE, camera, view modes |
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
- [**View All External Tools →**](https://kerinzeebart.github.io/MCPTest-with-UMCP.it/docs/tool-registry#external-tools)
114
 
115
- ### Plugin Tools (88 Features)
116
- Via Socket Server (port 9877)
 
 
117
 
118
- | Category | Tools | Description |
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
- [**View All Plugin Tools →**](https://kerinzeebart.github.io/MCPTest-with-UMCP.it/docs/tool-registry#plugin-tools)
129
 
130
- ---
 
 
 
 
 
 
131
 
132
- ## 🏗️ Architecture
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
- ### Comprehensive Test (5-8min)
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
- ### Live Monitoring
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
- **Required Plugins:**
275
- - Remote Control
276
- - Remote Control Web Interface
277
- - Python Script Plugin ✅
278
 
279
- **Config/DefaultEngine.ini:**
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
- **Important:** Restart Claude Desktop after configuration changes.
313
-
314
- ---
315
-
316
- ## 💻 Development Commands
317
 
318
- ### Testing
319
- ```bash
320
- # Quick connection test
321
- npm run test:connection
322
 
323
- # Comprehensive MCP test
324
- npm run test:mcp
325
 
326
- # All 164 tools test
327
- npm run test:all-tools
328
 
329
- # Docker tests
330
- docker-compose up mcp-test
331
- docker-compose up mcp-comprehensive
332
- docker-compose up mcp-all-tools
333
- ```
334
-
335
- ### Docker Operations
336
- ```bash
337
- # Build images
338
- docker-compose build
339
 
340
- # View logs
341
- docker-compose logs -f
342
 
343
- # Stop all containers
344
- docker-compose down
345
 
346
- # Clean up everything
347
- docker-compose down -v --rmi all
348
- ```
349
 
350
- ### Manual Testing
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
- ## 🎯 What's New
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())