grasant commited on
Commit
a92cb8f
·
verified ·
1 Parent(s): 8b7488c

Update plugin.py

Browse files
Files changed (1) hide show
  1. plugin.py +550 -535
plugin.py CHANGED
@@ -1,536 +1,551 @@
1
- """
2
- CanRun G-Assist Plugin - Official NVIDIA G-Assist Plugin
3
- Complete game compatibility analysis with Steam API, hardware detection, and S-tier performance assessment.
4
- """
5
-
6
- import json
7
- import logging
8
- import os
9
- import asyncio
10
- import sys
11
- from typing import Optional, Dict, Any
12
- from ctypes import byref, windll, wintypes
13
- from datetime import datetime
14
-
15
- # Add src to path for CanRun engine imports
16
- sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
17
-
18
- # Import CanRun engine - should always be available
19
- from src.canrun_engine import CanRunEngine
20
-
21
- # Configuration paths
22
- CONFIG_FILE = os.path.join(os.path.dirname(__file__), 'config.json')
23
- FALLBACK_CONFIG_FILE = os.path.join(
24
- os.environ.get("PROGRAMDATA", "."),
25
- r'NVIDIA Corporation\nvtopps\rise\plugins\canrun',
26
- 'config.json'
27
- )
28
-
29
- # Global config
30
- config = {}
31
-
32
- def load_config():
33
- """Load plugin configuration from local or system config."""
34
- global config
35
- try:
36
- # Try local config first
37
- if os.path.exists(CONFIG_FILE):
38
- with open(CONFIG_FILE, "r") as file:
39
- config = json.load(file)
40
- # Fallback to system config
41
- elif os.path.exists(FALLBACK_CONFIG_FILE):
42
- with open(FALLBACK_CONFIG_FILE, "r") as file:
43
- config = json.load(file)
44
- else:
45
- # Default config if no file found
46
- config = {
47
- "windows_pipe_config": {
48
- "STD_INPUT_HANDLE": -10,
49
- "STD_OUTPUT_HANDLE": -11,
50
- "BUFFER_SIZE": 4096
51
- },
52
- "logging_config": {
53
- "log_level": "INFO",
54
- "log_file": "canrun_g_assist.log"
55
- },
56
- "canrun_config": {
57
- "cache_dir": "cache",
58
- "enable_llm": True
59
- }
60
- }
61
- return config
62
- except Exception as e:
63
- logging.error(f"Error loading config: {e}")
64
- return {}
65
-
66
- def setup_logging():
67
- """Configure logging with timestamp format following NVIDIA pattern."""
68
- log_config = config.get("logging_config", {})
69
- log_file = os.path.join(os.environ.get("USERPROFILE", "."), log_config.get("log_file", "canrun_g_assist.log"))
70
-
71
- logging.basicConfig(
72
- filename=log_file,
73
- level=getattr(logging, log_config.get("log_level", "INFO")),
74
- format="%(asctime)s - %(levelname)s - %(message)s",
75
- filemode='a'
76
- )
77
-
78
- # Load config at startup
79
- config = load_config()
80
-
81
- # Windows pipe constants from config
82
- pipe_config = config.get("windows_pipe_config", {})
83
- STD_INPUT_HANDLE = pipe_config.get("STD_INPUT_HANDLE", -10)
84
- STD_OUTPUT_HANDLE = pipe_config.get("STD_OUTPUT_HANDLE", -11)
85
- BUFFER_SIZE = pipe_config.get("BUFFER_SIZE", 4096)
86
-
87
-
88
- def read_command() -> Optional[Dict[str, Any]]:
89
- """Read command from stdin - OFFICIAL NVIDIA IMPLEMENTATION"""
90
- try:
91
- # Read from stdin using the official protocol
92
- line = sys.stdin.readline()
93
- if not line:
94
- logging.error('Empty input received')
95
- return None
96
-
97
- logging.info(f'Received command: {line.strip()}')
98
- return json.loads(line)
99
-
100
- except json.JSONDecodeError as e:
101
- logging.error(f'Invalid JSON received: {e}')
102
- return None
103
- except Exception as e:
104
- logging.error(f'Error in read_command: {e}')
105
- return None
106
-
107
-
108
- def write_response(response: Dict[str, Any]) -> None:
109
- """Write response to stdout - OFFICIAL NVIDIA IMPLEMENTATION"""
110
- try:
111
- # CRITICAL: Add <<END>> marker for message termination
112
- message = json.dumps(response) + '<<END>>'
113
- sys.stdout.write(message)
114
- sys.stdout.flush()
115
- logging.info(f'Response sent: {len(message)} characters')
116
- except Exception as e:
117
- logging.error(f'Error writing response: {e}')
118
-
119
- def is_g_assist_environment() -> bool:
120
- """Check if running in G-Assist environment"""
121
- # In G-Assist environment, stdin is not a TTY
122
- return not sys.stdin.isatty()
123
-
124
-
125
- class CanRunGAssistPlugin:
126
- """Official G-Assist plugin for CanRun game compatibility checking."""
127
-
128
- def __init__(self):
129
- """Initialize CanRun G-Assist plugin with complete engine integration."""
130
- # Get CanRun configuration
131
- canrun_config = config.get("canrun_config", {})
132
-
133
- # Initialize CanRun engine with full feature set - always available
134
- self.canrun_engine = CanRunEngine(
135
- cache_dir=canrun_config.get("cache_dir", "cache"),
136
- enable_llm=canrun_config.get("enable_llm", True) # Enable G-Assist LLM integration
137
- )
138
- logging.info("CanRun engine initialized with complete feature set")
139
-
140
- async def check_game_compatibility(self, params: Dict[str, Any]) -> Dict[str, Any]:
141
- """Perform CanRun analysis using the full CanRun engine."""
142
- game_name = params.get("game_name", "").strip()
143
-
144
- # Handle force_refresh as either boolean or string
145
- force_refresh_param = params.get("force_refresh", False)
146
- if isinstance(force_refresh_param, str):
147
- force_refresh = force_refresh_param.lower() == "true"
148
- else:
149
- force_refresh = bool(force_refresh_param)
150
-
151
- if not game_name:
152
- return {
153
- "success": False,
154
- "message": "Game name is required for CanRun analysis"
155
- }
156
-
157
- logging.info(f"Starting CanRun analysis for: {game_name} (force_refresh: {force_refresh})")
158
-
159
- try:
160
- # Use the same CanRun engine to get the actual game-specific result
161
- # If force_refresh is True, don't use cache
162
- result = await self.canrun_engine.check_game_compatibility(game_name, use_cache=not force_refresh)
163
-
164
- if result:
165
- # Format the result directly - this ensures the game-specific performance tier is used
166
- formatted_result = self.format_canrun_response(result)
167
- return {
168
- "success": True,
169
- "message": formatted_result
170
- }
171
- else:
172
- return {
173
- "success": False,
174
- "message": f"Could not analyze game: {game_name}. Please check the game name and try again."
175
- }
176
-
177
- except Exception as e:
178
- logging.error(f"Error in game compatibility analysis: {e}")
179
- return {
180
- "success": False,
181
- "message": f"Error analyzing game: {str(e)}"
182
- }
183
-
184
- return {
185
- "success": True,
186
- "message": response_message
187
- }
188
-
189
- def detect_hardware(self, params: Dict[str, str]) -> Dict[str, Any]:
190
- """Provide simplified hardware detection focused on immediate response."""
191
- logging.info("Starting simplified hardware detection")
192
-
193
- # Provide immediate, useful hardware information
194
- hardware_message = """💻 SYSTEM HARDWARE DETECTION:
195
-
196
- 🖥️ GRAPHICS CARD:
197
- • GPU: RTX/GTX Series Detected
198
- • VRAM: 8GB+ Gaming Ready
199
- RTX Features: ✅ Supported
200
- • DLSS Support: ✅ Available
201
- • Driver Status: ✅ Compatible
202
-
203
- 🧠 PROCESSOR:
204
- CPU: Modern Gaming Processor
205
- Cores: Multi-core Gaming Ready
206
- Performance: Optimized
207
-
208
- 💾 MEMORY:
209
- RAM: 16GB+ Gaming Configuration
210
- • Speed: High-speed DDR4/DDR5
211
- Gaming Performance: ✅ Excellent
212
-
213
- 🖥️ DISPLAY:
214
- Resolution: High-resolution Gaming
215
- Refresh Rate: High-refresh Compatible
216
- G-Sync/FreeSync: ✅ Supported
217
-
218
- 💾 STORAGE:
219
- Type: NVMe SSD Gaming Ready
220
- Performance: Fast Loading
221
-
222
- 🖥️ SYSTEM:
223
- OS: Windows 11 Gaming Ready
224
- DirectX: DirectX 12 Ultimate
225
- G-Assist: Fully Compatible
226
-
227
- Hardware detection completed successfully. For detailed specifications, use the full CanRun desktop application."""
228
-
229
- return {
230
- "success": True,
231
- "message": hardware_message
232
- }
233
-
234
- def format_canrun_response(self, result) -> str:
235
- """Format CanRun result for G-Assist display with complete information."""
236
- try:
237
- # Extract performance tier and score
238
- tier = result.performance_prediction.tier.name if hasattr(result.performance_prediction, 'tier') else 'Unknown'
239
- score = int(result.performance_prediction.score) if hasattr(result.performance_prediction, 'score') else 0
240
-
241
- # Get compatibility status
242
- can_run = "✅ CAN RUN" if result.can_run_game() else "❌ CANNOT RUN"
243
- exceeds_recommended = result.exceeds_recommended_requirements()
244
-
245
- # Format comprehensive response
246
- original_query = result.game_name
247
- matched_name = result.game_requirements.game_name
248
-
249
- # Get actual Steam API game name if available
250
- steam_api_name = result.game_requirements.steam_api_name if hasattr(result.game_requirements, 'steam_api_name') and result.game_requirements.steam_api_name else matched_name
251
-
252
- # Determine if game name was matched differently from user query
253
- steam_api_info = ""
254
- if original_query.lower() != steam_api_name.lower():
255
- steam_api_info = f"(Steam found: {steam_api_name})"
256
-
257
- title_line = ""
258
- if result.can_run_game():
259
- if exceeds_recommended:
260
- title_line = f"✅ CANRUN: {original_query.upper()} will run EXCELLENTLY {steam_api_info}!"
261
- else:
262
- title_line = f"✅ CANRUN: {original_query.upper()} will run {steam_api_info}!"
263
- else:
264
- title_line = f"❌ CANNOT RUN {original_query.upper()} {steam_api_info}!"
265
-
266
- status_message = result.get_runnable_status_message()
267
-
268
- # Skip the status_message as it's redundant with the title line
269
- response = f"""{title_line}
270
-
271
- 🎮 YOUR SEARCH: {original_query}
272
- 🎮 STEAM MATCHED GAME: {steam_api_name}
273
-
274
- 🏆 PERFORMANCE TIER: {tier} ({score}/100)
275
-
276
- 💻 SYSTEM SPECIFICATIONS:
277
- CPU: {result.hardware_specs.cpu_model}
278
- • GPU: {result.hardware_specs.gpu_model} ({result.hardware_specs.gpu_vram_gb}GB VRAM)
279
- RAM: {result.hardware_specs.ram_total_gb}GB
280
- • RTX Features: {'✅ Supported' if result.hardware_specs.supports_rtx else '❌ Not Available'}
281
- DLSS Support: {'✅ Available' if result.hardware_specs.supports_dlss else '❌ Not Available'}
282
-
283
- 🎯 GAME REQUIREMENTS:
284
- Minimum GPU: {result.game_requirements.minimum_gpu}
285
- • Recommended GPU: {result.game_requirements.recommended_gpu}
286
- RAM Required: {result.game_requirements.minimum_ram_gb}GB (Min) / {result.game_requirements.recommended_ram_gb}GB (Rec)
287
- VRAM Required: {result.game_requirements.minimum_vram_gb}GB (Min) / {result.game_requirements.recommended_vram_gb}GB (Rec)
288
-
289
- PERFORMANCE PREDICTION:
290
- • Expected FPS: {getattr(result.performance_prediction, 'expected_fps', 'Unknown')}
291
- Recommended Settings: {getattr(result.performance_prediction, 'recommended_settings', 'Unknown')}
292
- Optimal Resolution: {getattr(result.performance_prediction, 'recommended_resolution', 'Unknown')}
293
- Performance Level: {'Exceeds Recommended' if exceeds_recommended else 'Meets Minimum' if result.can_run_game() else 'Below Minimum'}
294
-
295
- 🔧 OPTIMIZATION SUGGESTIONS:"""
296
-
297
- # Add optimization suggestions
298
- if hasattr(result.performance_prediction, 'upgrade_suggestions'):
299
- suggestions = result.performance_prediction.upgrade_suggestions[:3]
300
- for suggestion in suggestions:
301
- response += f"\n• {suggestion}"
302
- else:
303
- response += "\n• Update GPU drivers for optimal performance"
304
- if result.hardware_specs.supports_dlss:
305
- response += "\nEnable DLSS for significant performance boost"
306
- if result.hardware_specs.supports_rtx:
307
- response += "\nConsider RTX features for enhanced visuals"
308
-
309
- # Add compatibility analysis
310
- if hasattr(result, 'compatibility_analysis') and result.compatibility_analysis:
311
- if hasattr(result.compatibility_analysis, 'bottlenecks') and result.compatibility_analysis.bottlenecks:
312
- response += f"\n\n⚠️ POTENTIAL BOTTLENECKS:"
313
- for bottleneck in result.compatibility_analysis.bottlenecks[:2]:
314
- response += f"\n• {bottleneck.value}"
315
-
316
- # Add final verdict
317
- response += f"\n\n🎯 CANRUN VERDICT: {can_run}"
318
-
319
-
320
- # Make it clear if the Steam API returned something different than what was requested
321
- if steam_api_name.lower() != original_query.lower():
322
- response += f"\n\n🎮 NOTE: Steam found '{steam_api_name}' instead of '{original_query}'"
323
- response += f"\n Results shown are for '{steam_api_name}'"
324
-
325
- return response
326
-
327
- except Exception as e:
328
- logging.error(f"Error formatting CanRun response: {e}")
329
- return f"🎮 CANRUN ANALYSIS: {getattr(result, 'game_name', 'Unknown Game')}\n\n✅ Analysis completed but formatting error occurred.\nRaw result available in logs."
330
-
331
-
332
- async def handle_natural_language_query(query: str) -> str:
333
- """Handle natural language queries like 'canrun game?' and return formatted result."""
334
- # Extract game name from query
335
- game_name = query.strip()
336
-
337
- # Remove leading command patterns
338
- patterns = ["canrun ", "can run ", "can i run "]
339
- for pattern in patterns:
340
- if game_name.lower().startswith(pattern):
341
- game_name = game_name[len(pattern):].strip()
342
- break
343
-
344
- # Remove trailing question mark if present
345
- if game_name and game_name.endswith("?"):
346
- game_name = game_name[:-1].strip()
347
-
348
- if not game_name:
349
- return "Please specify a game name after 'canrun'."
350
-
351
- # Initialize plugin
352
- plugin = CanRunGAssistPlugin()
353
-
354
- # Use the same logic as in app.py for fresh analysis
355
- has_number = any(c.isdigit() for c in game_name)
356
- force_refresh = has_number # Force refresh for numbered games
357
-
358
- # Create params
359
- params = {"game_name": game_name, "force_refresh": force_refresh}
360
-
361
- # Execute compatibility check
362
- response = await plugin.check_game_compatibility(params)
363
-
364
- # Return the formatted message (same as what Gradio would display)
365
- if response.get("success", False):
366
- return response.get("message", "Analysis completed successfully.")
367
- else:
368
- return response.get("message", f"Could not analyze game: {game_name}. Please check the game name and try again.")
369
-
370
- def main():
371
- """Main plugin execution loop - OFFICIAL NVIDIA IMPLEMENTATION"""
372
- setup_logging()
373
- logging.info("CanRun Plugin Started")
374
-
375
- # Check if command line arguments were provided
376
- if len(sys.argv) > 1:
377
- # Handle command-line arguments in "canrun game?" format
378
- args = sys.argv[1:]
379
-
380
- # Process query
381
- query = " ".join(args)
382
- game_query = ""
383
-
384
- # Check if the query matches our expected format "canrun game?"
385
- # This will handle both "canrun game?" and just "game?"
386
- if args[0].lower() == "canrun" and len(args) > 1:
387
- # Extract just the game name after "canrun"
388
- game_query = " ".join(args[1:])
389
- elif query.lower().startswith("canrun "):
390
- # Handle case where "canrun" might be part of a single argument
391
- game_query = query[7:].strip()
392
- else:
393
- # Assume the entire query is the game name
394
- game_query = query
395
-
396
- # Always remove question mark from the end for processing
397
- game_query = game_query.rstrip("?").strip()
398
-
399
- # Debugging output to help troubleshoot argument issues
400
- logging.info(f"Command line args: {args}")
401
- logging.info(f"Processed game query: {game_query}")
402
-
403
- # Create event loop for async operation
404
- loop = asyncio.new_event_loop()
405
- asyncio.set_event_loop(loop)
406
-
407
- # Run the query and print result directly to stdout
408
- result = loop.run_until_complete(handle_natural_language_query(game_query))
409
- print(result)
410
- loop.close()
411
- return
412
-
413
- # Check if running in G-Assist environment
414
- in_g_assist = is_g_assist_environment()
415
- logging.info(f"Running in G-Assist environment: {in_g_assist}")
416
-
417
- # Initialize plugin - CanRun engine always available
418
- plugin = CanRunGAssistPlugin()
419
- logging.info("CanRun plugin initialized successfully")
420
-
421
- # If not in G-Assist environment, exit - we only care about G-Assist mode
422
- if not in_g_assist:
423
- print("This is a G-Assist plugin. Please run through G-Assist.")
424
- return
425
-
426
- # G-Assist protocol mode
427
- while True:
428
- command = read_command()
429
- if command is None:
430
- continue
431
-
432
- # Handle G-Assist input in different formats
433
- if "tool_calls" in command:
434
- # Standard G-Assist protocol format with tool_calls
435
- for tool_call in command.get("tool_calls", []):
436
- func = tool_call.get("func")
437
- params = tool_call.get("params", {})
438
-
439
- if func == "check_compatibility":
440
- # For async function, we need to run in an event loop
441
- loop = asyncio.new_event_loop()
442
- asyncio.set_event_loop(loop)
443
- response = loop.run_until_complete(plugin.check_game_compatibility(params))
444
- write_response(response)
445
- loop.close()
446
- elif func == "detect_hardware":
447
- response = plugin.detect_hardware(params)
448
- write_response(response)
449
- elif func == "auto_detect":
450
- # Handle natural language input like "canrun game?"
451
- user_input = params.get("user_input", "")
452
- logging.info(f"Auto-detect received: {user_input}")
453
-
454
- # Extract game name from queries like "canrun game?"
455
- game_name = user_input
456
- if "canrun" in user_input.lower():
457
- # Remove "canrun" prefix and extract game name
458
- parts = user_input.lower().split("canrun")
459
- if len(parts) > 1:
460
- game_name = parts[1].strip()
461
-
462
- # Remove question mark if present
463
- game_name = game_name.rstrip("?").strip()
464
-
465
- if game_name:
466
- # Create compatibility check params
467
- compat_params = {"game_name": game_name}
468
-
469
- # For async function, we need to run in an event loop
470
- loop = asyncio.new_event_loop()
471
- asyncio.set_event_loop(loop)
472
- response = loop.run_until_complete(plugin.check_game_compatibility(compat_params))
473
- write_response(response)
474
- loop.close()
475
- else:
476
- write_response({
477
- "success": False,
478
- "message": "Could not identify a game name in your query. Please try 'Can I run <game name>?'"
479
- })
480
- elif func == "shutdown":
481
- logging.info("Shutdown command received. Exiting.")
482
- return
483
- else:
484
- logging.warning(f"Unknown function: {func}")
485
- write_response({
486
- "success": False,
487
- "message": f"Unknown function: {func}"
488
- })
489
- elif "user_input" in command:
490
- # Alternative format with direct user_input field
491
- user_input = command.get("user_input", "")
492
- logging.info(f"Direct user input received: {user_input}")
493
-
494
- # Check if this is a game compatibility query
495
- if "canrun" in user_input.lower() or "can run" in user_input.lower() or "can i run" in user_input.lower():
496
- # Extract game name
497
- game_name = ""
498
- for prefix in ["canrun ", "can run ", "can i run "]:
499
- if user_input.lower().startswith(prefix):
500
- game_name = user_input[len(prefix):].strip()
501
- break
502
-
503
- # If no prefix found but contains "canrun" somewhere
504
- if not game_name and "canrun" in user_input.lower():
505
- parts = user_input.lower().split("canrun")
506
- if len(parts) > 1:
507
- game_name = parts[1].strip()
508
-
509
- # Remove question mark if present
510
- game_name = game_name.rstrip("?").strip()
511
-
512
- if game_name:
513
- # Create compatibility check params
514
- compat_params = {"game_name": game_name}
515
-
516
- # For async function, we need to run in an event loop
517
- loop = asyncio.new_event_loop()
518
- asyncio.set_event_loop(loop)
519
- response = loop.run_until_complete(plugin.check_game_compatibility(compat_params))
520
- write_response(response)
521
- loop.close()
522
- else:
523
- write_response({
524
- "success": False,
525
- "message": "Could not identify a game name in your query. Please try 'Can I run <game name>?'"
526
- })
527
- else:
528
- # Not a game compatibility query
529
- write_response({
530
- "success": False,
531
- "message": "I can check if your system can run games. Try asking 'Can I run <game name>?'"
532
- })
533
-
534
-
535
- if __name__ == "__main__":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
536
  main()
 
1
+ """
2
+ CanRun G-Assist Plugin - Official NVIDIA G-Assist Plugin
3
+ Complete game compatibility analysis with Steam API, hardware detection, and S-tier performance assessment.
4
+ """
5
+
6
+ import json
7
+ import logging
8
+ import os
9
+ import asyncio
10
+ import sys
11
+ import platform
12
+ from typing import Optional, Dict, Any
13
+ from datetime import datetime
14
+
15
+ # Add src to path for CanRun engine imports
16
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
17
+
18
+ # Platform detection
19
+ IS_WINDOWS = platform.system() == "Windows"
20
+
21
+ # Import CanRun engine - should always be available
22
+ from src.canrun_engine import CanRunEngine
23
+
24
+ # Configuration paths
25
+ CONFIG_FILE = os.path.join(os.path.dirname(__file__), 'config.json')
26
+ # Create platform-independent paths
27
+ if IS_WINDOWS:
28
+ FALLBACK_CONFIG_FILE = os.path.join(
29
+ os.environ.get("PROGRAMDATA", "."),
30
+ r'NVIDIA Corporation\nvtopps\rise\plugins\canrun',
31
+ 'config.json'
32
+ )
33
+ else:
34
+ # For Linux/macOS, use a standard location
35
+ FALLBACK_CONFIG_FILE = os.path.join(
36
+ os.environ.get("HOME", "."),
37
+ ".config/canrun",
38
+ 'config.json'
39
+ )
40
+
41
+ # Global config
42
+ config = {}
43
+
44
+ def load_config():
45
+ """Load plugin configuration from local or system config."""
46
+ global config
47
+ try:
48
+ # Try local config first
49
+ if os.path.exists(CONFIG_FILE):
50
+ with open(CONFIG_FILE, "r") as file:
51
+ config = json.load(file)
52
+ # Fallback to system config
53
+ elif os.path.exists(FALLBACK_CONFIG_FILE):
54
+ with open(FALLBACK_CONFIG_FILE, "r") as file:
55
+ config = json.load(file)
56
+ else:
57
+ # Default config if no file found
58
+ config = {
59
+ "windows_pipe_config": {
60
+ "STD_INPUT_HANDLE": -10,
61
+ "STD_OUTPUT_HANDLE": -11,
62
+ "BUFFER_SIZE": 4096
63
+ },
64
+ "logging_config": {
65
+ "log_level": "INFO",
66
+ "log_file": "canrun_g_assist.log"
67
+ },
68
+ "canrun_config": {
69
+ "cache_dir": "cache",
70
+ "enable_llm": True
71
+ }
72
+ }
73
+ return config
74
+ except Exception as e:
75
+ logging.error(f"Error loading config: {e}")
76
+ return {}
77
+
78
+ def setup_logging():
79
+ """Configure logging with timestamp format following NVIDIA pattern."""
80
+ log_config = config.get("logging_config", {})
81
+ # Use platform-independent home directory
82
+ home_dir = os.environ.get("USERPROFILE" if IS_WINDOWS else "HOME", ".")
83
+ log_file = os.path.join(home_dir, log_config.get("log_file", "canrun_g_assist.log"))
84
+
85
+ logging.basicConfig(
86
+ filename=log_file,
87
+ level=getattr(logging, log_config.get("log_level", "INFO")),
88
+ format="%(asctime)s - %(levelname)s - %(message)s",
89
+ filemode='a'
90
+ )
91
+
92
+ # Load config at startup
93
+ config = load_config()
94
+ # Pipe constants from config - only used on Windows
95
+ if IS_WINDOWS:
96
+ pipe_config = config.get("windows_pipe_config", {})
97
+ STD_INPUT_HANDLE = pipe_config.get("STD_INPUT_HANDLE", -10)
98
+ STD_OUTPUT_HANDLE = pipe_config.get("STD_OUTPUT_HANDLE", -11)
99
+ BUFFER_SIZE = pipe_config.get("BUFFER_SIZE", 4096)
100
+
101
+
102
+
103
+ def read_command() -> Optional[Dict[str, Any]]:
104
+ """Read command from stdin - OFFICIAL NVIDIA IMPLEMENTATION"""
105
+ try:
106
+ # Read from stdin using the official protocol
107
+ line = sys.stdin.readline()
108
+ if not line:
109
+ logging.error('Empty input received')
110
+ return None
111
+
112
+ logging.info(f'Received command: {line.strip()}')
113
+ return json.loads(line)
114
+
115
+ except json.JSONDecodeError as e:
116
+ logging.error(f'Invalid JSON received: {e}')
117
+ return None
118
+ except Exception as e:
119
+ logging.error(f'Error in read_command: {e}')
120
+ return None
121
+
122
+
123
+ def write_response(response: Dict[str, Any]) -> None:
124
+ """Write response to stdout - OFFICIAL NVIDIA IMPLEMENTATION"""
125
+ try:
126
+ # CRITICAL: Add <<END>> marker for message termination
127
+ message = json.dumps(response) + '<<END>>'
128
+ sys.stdout.write(message)
129
+ sys.stdout.flush()
130
+ logging.info(f'Response sent: {len(message)} characters')
131
+ except Exception as e:
132
+ logging.error(f'Error writing response: {e}')
133
+
134
+ def is_g_assist_environment() -> bool:
135
+ """Check if running in G-Assist environment"""
136
+ # In G-Assist environment, stdin is not a TTY
137
+ return not sys.stdin.isatty()
138
+
139
+
140
+ class CanRunGAssistPlugin:
141
+ """Official G-Assist plugin for CanRun game compatibility checking."""
142
+
143
+ def __init__(self):
144
+ """Initialize CanRun G-Assist plugin with complete engine integration."""
145
+ # Get CanRun configuration
146
+ canrun_config = config.get("canrun_config", {})
147
+
148
+ # Initialize CanRun engine with full feature set - always available
149
+ self.canrun_engine = CanRunEngine(
150
+ cache_dir=canrun_config.get("cache_dir", "cache"),
151
+ enable_llm=canrun_config.get("enable_llm", True) # Enable G-Assist LLM integration
152
+ )
153
+ logging.info("CanRun engine initialized with complete feature set")
154
+
155
+ async def check_game_compatibility(self, params: Dict[str, Any]) -> Dict[str, Any]:
156
+ """Perform CanRun analysis using the full CanRun engine."""
157
+ game_name = params.get("game_name", "").strip()
158
+
159
+ # Handle force_refresh as either boolean or string
160
+ force_refresh_param = params.get("force_refresh", False)
161
+ if isinstance(force_refresh_param, str):
162
+ force_refresh = force_refresh_param.lower() == "true"
163
+ else:
164
+ force_refresh = bool(force_refresh_param)
165
+
166
+ if not game_name:
167
+ return {
168
+ "success": False,
169
+ "message": "Game name is required for CanRun analysis"
170
+ }
171
+
172
+ logging.info(f"Starting CanRun analysis for: {game_name} (force_refresh: {force_refresh})")
173
+
174
+ try:
175
+ # Use the same CanRun engine to get the actual game-specific result
176
+ # If force_refresh is True, don't use cache
177
+ result = await self.canrun_engine.check_game_compatibility(game_name, use_cache=not force_refresh)
178
+
179
+ if result:
180
+ # Format the result directly - this ensures the game-specific performance tier is used
181
+ formatted_result = self.format_canrun_response(result)
182
+ return {
183
+ "success": True,
184
+ "message": formatted_result
185
+ }
186
+ else:
187
+ return {
188
+ "success": False,
189
+ "message": f"Could not analyze game: {game_name}. Please check the game name and try again."
190
+ }
191
+
192
+ except Exception as e:
193
+ logging.error(f"Error in game compatibility analysis: {e}")
194
+ return {
195
+ "success": False,
196
+ "message": f"Error analyzing game: {str(e)}"
197
+ }
198
+
199
+ return {
200
+ "success": True,
201
+ "message": response_message
202
+ }
203
+
204
+ def detect_hardware(self, params: Dict[str, str]) -> Dict[str, Any]:
205
+ """Provide simplified hardware detection focused on immediate response."""
206
+ logging.info("Starting simplified hardware detection")
207
+
208
+ # Provide immediate, useful hardware information
209
+ hardware_message = """💻 SYSTEM HARDWARE DETECTION:
210
+
211
+ 🖥️ GRAPHICS CARD:
212
+ • GPU: RTX/GTX Series Detected
213
+ VRAM: 8GB+ Gaming Ready
214
+ RTX Features: Supported
215
+ DLSS Support: Available
216
+ Driver Status: ✅ Compatible
217
+
218
+ 🧠 PROCESSOR:
219
+ CPU: Modern Gaming Processor
220
+ Cores: Multi-core Gaming Ready
221
+ • Performance: ✅ Optimized
222
+
223
+ 💾 MEMORY:
224
+ RAM: 16GB+ Gaming Configuration
225
+ Speed: High-speed DDR4/DDR5
226
+ • Gaming Performance: ✅ Excellent
227
+
228
+ 🖥️ DISPLAY:
229
+ Resolution: High-resolution Gaming
230
+ • Refresh Rate: High-refresh Compatible
231
+ • G-Sync/FreeSync: ✅ Supported
232
+
233
+ 💾 STORAGE:
234
+ Type: NVMe SSD Gaming Ready
235
+ Performance: Fast Loading
236
+
237
+ 🖥️ SYSTEM:
238
+ OS: Windows 11 Gaming Ready
239
+ DirectX: DirectX 12 Ultimate
240
+ • G-Assist: ✅ Fully Compatible
241
+
242
+ Hardware detection completed successfully. For detailed specifications, use the full CanRun desktop application."""
243
+
244
+ return {
245
+ "success": True,
246
+ "message": hardware_message
247
+ }
248
+
249
+ def format_canrun_response(self, result) -> str:
250
+ """Format CanRun result for G-Assist display with complete information."""
251
+ try:
252
+ # Extract performance tier and score
253
+ tier = result.performance_prediction.tier.name if hasattr(result.performance_prediction, 'tier') else 'Unknown'
254
+ score = int(result.performance_prediction.score) if hasattr(result.performance_prediction, 'score') else 0
255
+
256
+ # Get compatibility status
257
+ can_run = "✅ CAN RUN" if result.can_run_game() else "❌ CANNOT RUN"
258
+ exceeds_recommended = result.exceeds_recommended_requirements()
259
+
260
+ # Format comprehensive response
261
+ original_query = result.game_name
262
+ matched_name = result.game_requirements.game_name
263
+
264
+ # Get actual Steam API game name if available
265
+ steam_api_name = result.game_requirements.steam_api_name if hasattr(result.game_requirements, 'steam_api_name') and result.game_requirements.steam_api_name else matched_name
266
+
267
+ # Determine if game name was matched differently from user query
268
+ steam_api_info = ""
269
+ if original_query.lower() != steam_api_name.lower():
270
+ steam_api_info = f"(Steam found: {steam_api_name})"
271
+
272
+ title_line = ""
273
+ if result.can_run_game():
274
+ if exceeds_recommended:
275
+ title_line = f"✅ CANRUN: {original_query.upper()} will run EXCELLENTLY {steam_api_info}!"
276
+ else:
277
+ title_line = f"✅ CANRUN: {original_query.upper()} will run {steam_api_info}!"
278
+ else:
279
+ title_line = f"❌ CANNOT RUN {original_query.upper()} {steam_api_info}!"
280
+
281
+ status_message = result.get_runnable_status_message()
282
+
283
+ # Skip the status_message as it's redundant with the title line
284
+ response = f"""{title_line}
285
+
286
+ 🎮 YOUR SEARCH: {original_query}
287
+ 🎮 STEAM MATCHED GAME: {steam_api_name}
288
+
289
+ 🏆 PERFORMANCE TIER: {tier} ({score}/100)
290
+
291
+ 💻 SYSTEM SPECIFICATIONS:
292
+ CPU: {result.hardware_specs.cpu_model}
293
+ GPU: {result.hardware_specs.gpu_model} ({result.hardware_specs.gpu_vram_gb}GB VRAM)
294
+ • RAM: {result.hardware_specs.ram_total_gb}GB
295
+ RTX Features: {'✅ Supported' if result.hardware_specs.supports_rtx else '❌ Not Available'}
296
+ • DLSS Support: {'✅ Available' if result.hardware_specs.supports_dlss else '❌ Not Available'}
297
+
298
+ 🎯 GAME REQUIREMENTS:
299
+ Minimum GPU: {result.game_requirements.minimum_gpu}
300
+ Recommended GPU: {result.game_requirements.recommended_gpu}
301
+ RAM Required: {result.game_requirements.minimum_ram_gb}GB (Min) / {result.game_requirements.recommended_ram_gb}GB (Rec)
302
+ • VRAM Required: {result.game_requirements.minimum_vram_gb}GB (Min) / {result.game_requirements.recommended_vram_gb}GB (Rec)
303
+
304
+ PERFORMANCE PREDICTION:
305
+ Expected FPS: {getattr(result.performance_prediction, 'expected_fps', 'Unknown')}
306
+ Recommended Settings: {getattr(result.performance_prediction, 'recommended_settings', 'Unknown')}
307
+ Optimal Resolution: {getattr(result.performance_prediction, 'recommended_resolution', 'Unknown')}
308
+ • Performance Level: {'Exceeds Recommended' if exceeds_recommended else 'Meets Minimum' if result.can_run_game() else 'Below Minimum'}
309
+
310
+ 🔧 OPTIMIZATION SUGGESTIONS:"""
311
+
312
+ # Add optimization suggestions
313
+ if hasattr(result.performance_prediction, 'upgrade_suggestions'):
314
+ suggestions = result.performance_prediction.upgrade_suggestions[:3]
315
+ for suggestion in suggestions:
316
+ response += f"\n• {suggestion}"
317
+ else:
318
+ response += "\n• Update GPU drivers for optimal performance"
319
+ if result.hardware_specs.supports_dlss:
320
+ response += "\n• Enable DLSS for significant performance boost"
321
+ if result.hardware_specs.supports_rtx:
322
+ response += "\n Consider RTX features for enhanced visuals"
323
+
324
+ # Add compatibility analysis
325
+ if hasattr(result, 'compatibility_analysis') and result.compatibility_analysis:
326
+ if hasattr(result.compatibility_analysis, 'bottlenecks') and result.compatibility_analysis.bottlenecks:
327
+ response += f"\n\n⚠️ POTENTIAL BOTTLENECKS:"
328
+ for bottleneck in result.compatibility_analysis.bottlenecks[:2]:
329
+ response += f"\n {bottleneck.value}"
330
+
331
+ # Add final verdict
332
+ response += f"\n\n🎯 CANRUN VERDICT: {can_run}"
333
+
334
+
335
+ # Make it clear if the Steam API returned something different than what was requested
336
+ if steam_api_name.lower() != original_query.lower():
337
+ response += f"\n\n🎮 NOTE: Steam found '{steam_api_name}' instead of '{original_query}'"
338
+ response += f"\n Results shown are for '{steam_api_name}'"
339
+
340
+ return response
341
+
342
+ except Exception as e:
343
+ logging.error(f"Error formatting CanRun response: {e}")
344
+ return f"🎮 CANRUN ANALYSIS: {getattr(result, 'game_name', 'Unknown Game')}\n\n✅ Analysis completed but formatting error occurred.\nRaw result available in logs."
345
+
346
+
347
+ async def handle_natural_language_query(query: str) -> str:
348
+ """Handle natural language queries like 'canrun game?' and return formatted result."""
349
+ # Extract game name from query
350
+ game_name = query.strip()
351
+
352
+ # Remove leading command patterns
353
+ patterns = ["canrun ", "can run ", "can i run "]
354
+ for pattern in patterns:
355
+ if game_name.lower().startswith(pattern):
356
+ game_name = game_name[len(pattern):].strip()
357
+ break
358
+
359
+ # Remove trailing question mark if present
360
+ if game_name and game_name.endswith("?"):
361
+ game_name = game_name[:-1].strip()
362
+
363
+ if not game_name:
364
+ return "Please specify a game name after 'canrun'."
365
+
366
+ # Initialize plugin
367
+ plugin = CanRunGAssistPlugin()
368
+
369
+ # Use the same logic as in app.py for fresh analysis
370
+ has_number = any(c.isdigit() for c in game_name)
371
+ force_refresh = has_number # Force refresh for numbered games
372
+
373
+ # Create params
374
+ params = {"game_name": game_name, "force_refresh": force_refresh}
375
+
376
+ # Execute compatibility check
377
+ response = await plugin.check_game_compatibility(params)
378
+
379
+ # Return the formatted message (same as what Gradio would display)
380
+ if response.get("success", False):
381
+ return response.get("message", "Analysis completed successfully.")
382
+ else:
383
+ return response.get("message", f"Could not analyze game: {game_name}. Please check the game name and try again.")
384
+
385
+ def main():
386
+ """Main plugin execution loop - OFFICIAL NVIDIA IMPLEMENTATION"""
387
+ setup_logging()
388
+ logging.info("CanRun Plugin Started")
389
+
390
+ # Check if command line arguments were provided
391
+ if len(sys.argv) > 1:
392
+ # Handle command-line arguments in "canrun game?" format
393
+ args = sys.argv[1:]
394
+
395
+ # Process query
396
+ query = " ".join(args)
397
+ game_query = ""
398
+
399
+ # Check if the query matches our expected format "canrun game?"
400
+ # This will handle both "canrun game?" and just "game?"
401
+ if args[0].lower() == "canrun" and len(args) > 1:
402
+ # Extract just the game name after "canrun"
403
+ game_query = " ".join(args[1:])
404
+ elif query.lower().startswith("canrun "):
405
+ # Handle case where "canrun" might be part of a single argument
406
+ game_query = query[7:].strip()
407
+ else:
408
+ # Assume the entire query is the game name
409
+ game_query = query
410
+
411
+ # Always remove question mark from the end for processing
412
+ game_query = game_query.rstrip("?").strip()
413
+
414
+ # Debugging output to help troubleshoot argument issues
415
+ logging.info(f"Command line args: {args}")
416
+ logging.info(f"Processed game query: {game_query}")
417
+
418
+ # Create event loop for async operation
419
+ loop = asyncio.new_event_loop()
420
+ asyncio.set_event_loop(loop)
421
+
422
+ # Run the query and print result directly to stdout
423
+ result = loop.run_until_complete(handle_natural_language_query(game_query))
424
+ print(result)
425
+ loop.close()
426
+ return
427
+
428
+ # Check if running in G-Assist environment
429
+ in_g_assist = is_g_assist_environment()
430
+ logging.info(f"Running in G-Assist environment: {in_g_assist}")
431
+
432
+ # Initialize plugin - CanRun engine always available
433
+ plugin = CanRunGAssistPlugin()
434
+ logging.info("CanRun plugin initialized successfully")
435
+
436
+ # If not in G-Assist environment, exit - we only care about G-Assist mode
437
+ if not in_g_assist:
438
+ print("This is a G-Assist plugin. Please run through G-Assist.")
439
+ return
440
+
441
+ # G-Assist protocol mode
442
+ while True:
443
+ command = read_command()
444
+ if command is None:
445
+ continue
446
+
447
+ # Handle G-Assist input in different formats
448
+ if "tool_calls" in command:
449
+ # Standard G-Assist protocol format with tool_calls
450
+ for tool_call in command.get("tool_calls", []):
451
+ func = tool_call.get("func")
452
+ params = tool_call.get("params", {})
453
+
454
+ if func == "check_compatibility":
455
+ # For async function, we need to run in an event loop
456
+ loop = asyncio.new_event_loop()
457
+ asyncio.set_event_loop(loop)
458
+ response = loop.run_until_complete(plugin.check_game_compatibility(params))
459
+ write_response(response)
460
+ loop.close()
461
+ elif func == "detect_hardware":
462
+ response = plugin.detect_hardware(params)
463
+ write_response(response)
464
+ elif func == "auto_detect":
465
+ # Handle natural language input like "canrun game?"
466
+ user_input = params.get("user_input", "")
467
+ logging.info(f"Auto-detect received: {user_input}")
468
+
469
+ # Extract game name from queries like "canrun game?"
470
+ game_name = user_input
471
+ if "canrun" in user_input.lower():
472
+ # Remove "canrun" prefix and extract game name
473
+ parts = user_input.lower().split("canrun")
474
+ if len(parts) > 1:
475
+ game_name = parts[1].strip()
476
+
477
+ # Remove question mark if present
478
+ game_name = game_name.rstrip("?").strip()
479
+
480
+ if game_name:
481
+ # Create compatibility check params
482
+ compat_params = {"game_name": game_name}
483
+
484
+ # For async function, we need to run in an event loop
485
+ loop = asyncio.new_event_loop()
486
+ asyncio.set_event_loop(loop)
487
+ response = loop.run_until_complete(plugin.check_game_compatibility(compat_params))
488
+ write_response(response)
489
+ loop.close()
490
+ else:
491
+ write_response({
492
+ "success": False,
493
+ "message": "Could not identify a game name in your query. Please try 'Can I run <game name>?'"
494
+ })
495
+ elif func == "shutdown":
496
+ logging.info("Shutdown command received. Exiting.")
497
+ return
498
+ else:
499
+ logging.warning(f"Unknown function: {func}")
500
+ write_response({
501
+ "success": False,
502
+ "message": f"Unknown function: {func}"
503
+ })
504
+ elif "user_input" in command:
505
+ # Alternative format with direct user_input field
506
+ user_input = command.get("user_input", "")
507
+ logging.info(f"Direct user input received: {user_input}")
508
+
509
+ # Check if this is a game compatibility query
510
+ if "canrun" in user_input.lower() or "can run" in user_input.lower() or "can i run" in user_input.lower():
511
+ # Extract game name
512
+ game_name = ""
513
+ for prefix in ["canrun ", "can run ", "can i run "]:
514
+ if user_input.lower().startswith(prefix):
515
+ game_name = user_input[len(prefix):].strip()
516
+ break
517
+
518
+ # If no prefix found but contains "canrun" somewhere
519
+ if not game_name and "canrun" in user_input.lower():
520
+ parts = user_input.lower().split("canrun")
521
+ if len(parts) > 1:
522
+ game_name = parts[1].strip()
523
+
524
+ # Remove question mark if present
525
+ game_name = game_name.rstrip("?").strip()
526
+
527
+ if game_name:
528
+ # Create compatibility check params
529
+ compat_params = {"game_name": game_name}
530
+
531
+ # For async function, we need to run in an event loop
532
+ loop = asyncio.new_event_loop()
533
+ asyncio.set_event_loop(loop)
534
+ response = loop.run_until_complete(plugin.check_game_compatibility(compat_params))
535
+ write_response(response)
536
+ loop.close()
537
+ else:
538
+ write_response({
539
+ "success": False,
540
+ "message": "Could not identify a game name in your query. Please try 'Can I run <game name>?'"
541
+ })
542
+ else:
543
+ # Not a game compatibility query
544
+ write_response({
545
+ "success": False,
546
+ "message": "I can check if your system can run games. Try asking 'Can I run <game name>?'"
547
+ })
548
+
549
+
550
+ if __name__ == "__main__":
551
  main()