Skip to content

Commit 299948a

Browse files
fix: add comprehensive Windows support with minimal changes
- shell_tools.py: Replace os.uname() with platform.system(), fix Unix path handling, add proper Windows command parsing - deploy.py: Replace Unix command substitution with cross-platform subprocess calls, handle Windows authentication - mcp.py: Add Windows-aware command parsing, conditional locale variables, NPX variant detection All changes maintain full backward compatibility for Unix/Linux/macOS. Co-authored-by: Mervin Praison <[email protected]>
1 parent f676855 commit 299948a

File tree

3 files changed

+101
-21
lines changed

3 files changed

+101
-21
lines changed

src/praisonai-agents/praisonaiagents/mcp/mcp.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import logging
88
import os
99
import re
10+
import platform
1011
from typing import Any, List, Optional, Callable, Iterable, Union
1112
from functools import wraps, partial
1213

@@ -199,7 +200,13 @@ def __init__(self, command_or_string=None, args=None, *, command=None, timeout=6
199200
# Handle the single string format for stdio client
200201
if isinstance(command_or_string, str) and args is None:
201202
# Split the string into command and args using shell-like parsing
202-
parts = shlex.split(command_or_string)
203+
if platform.system() == 'Windows':
204+
# Use shlex with posix=False for Windows to handle quotes and paths with spaces
205+
parts = shlex.split(command_or_string, posix=False)
206+
# Remove quotes from parts if present (Windows shlex keeps them)
207+
parts = [part.strip('"') for part in parts]
208+
else:
209+
parts = shlex.split(command_or_string)
203210
if not parts:
204211
raise ValueError("Empty command string")
205212

@@ -217,11 +224,17 @@ def __init__(self, command_or_string=None, args=None, *, command=None, timeout=6
217224
env = kwargs.get('env', {})
218225
if not env:
219226
env = os.environ.copy()
220-
env.update({
221-
'PYTHONIOENCODING': 'utf-8',
222-
'LC_ALL': 'C.UTF-8',
223-
'LANG': 'C.UTF-8'
224-
})
227+
228+
# Always set Python encoding
229+
env['PYTHONIOENCODING'] = 'utf-8'
230+
231+
# Only set locale variables on Unix systems
232+
if platform.system() != 'Windows':
233+
env.update({
234+
'LC_ALL': 'C.UTF-8',
235+
'LANG': 'C.UTF-8'
236+
})
237+
225238
kwargs['env'] = env
226239

227240
self.server_params = StdioServerParameters(
@@ -236,7 +249,14 @@ def __init__(self, command_or_string=None, args=None, *, command=None, timeout=6
236249
print(f"Warning: MCP initialization timed out after {self.timeout} seconds")
237250

238251
# Automatically detect if this is an NPX command
239-
self.is_npx = cmd == 'npx' or (isinstance(cmd, str) and os.path.basename(cmd) == 'npx')
252+
base_cmd = os.path.basename(cmd) if isinstance(cmd, str) else cmd
253+
# Check for npx with or without Windows extensions
254+
npx_variants = ['npx', 'npx.cmd', 'npx.exe']
255+
if platform.system() == 'Windows' and isinstance(base_cmd, str):
256+
# Case-insensitive comparison on Windows
257+
self.is_npx = base_cmd.lower() in [v.lower() for v in npx_variants]
258+
else:
259+
self.is_npx = base_cmd in npx_variants
240260

241261
# For NPX-based MCP servers, use a different approach
242262
if self.is_npx:

src/praisonai-agents/praisonaiagents/tools/shell_tools.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
import logging
1313
import os
1414
import time
15+
import platform
16+
import psutil
1517
from typing import Dict, List, Optional, Union
1618
from ..approval import require_approval
1719

@@ -58,7 +60,12 @@ def execute_command(
5860
try:
5961
# Split command if not using shell
6062
if not shell:
61-
command = shlex.split(command)
63+
# Use shlex.split with appropriate posix flag
64+
if platform.system() == 'Windows':
65+
# Use shlex with posix=False for Windows to handle quotes properly
66+
command = shlex.split(command, posix=False)
67+
else:
68+
command = shlex.split(command)
6269

6370
# Set up process environment
6471
process_env = os.environ.copy()
@@ -201,7 +208,9 @@ def get_system_info(self) -> Dict[str, Union[float, int, str, Dict]]:
201208
try:
202209
cpu_percent = psutil.cpu_percent(interval=1)
203210
memory = psutil.virtual_memory()
204-
disk = psutil.disk_usage('/')
211+
# Use appropriate root path for the OS
212+
root_path = os.path.abspath(os.sep)
213+
disk = psutil.disk_usage(root_path)
205214

206215
return {
207216
'cpu': {
@@ -223,7 +232,7 @@ def get_system_info(self) -> Dict[str, Union[float, int, str, Dict]]:
223232
'percent': disk.percent
224233
},
225234
'boot_time': psutil.boot_time(),
226-
'platform': os.uname().sysname
235+
'platform': platform.system()
227236
}
228237
except Exception as e:
229238
error_msg = f"Error getting system info: {str(e)}"
@@ -246,7 +255,11 @@ def get_system_info(self) -> Dict[str, Union[float, int, str, Dict]]:
246255
# 1. Execute command
247256
print("1. Command Execution")
248257
print("------------------------------")
249-
result = execute_command("ls -la")
258+
# Cross-platform directory listing
259+
if platform.system() == 'Windows':
260+
result = execute_command("dir")
261+
else:
262+
result = execute_command("ls -la")
250263
print(f"Success: {result['success']}")
251264
print(f"Output:\n{result['stdout']}")
252265
if result['stderr']:

src/praisonai/praisonai/deploy.py

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import subprocess
22
import os
3+
import platform
34
from dotenv import load_dotenv
45

56
class CloudDeployer:
@@ -116,20 +117,66 @@ def run_commands(self):
116117
self.create_api_file()
117118
self.create_dockerfile()
118119
"""Runs a sequence of shell commands for deployment, continues on error."""
120+
121+
# Get project ID upfront for Windows compatibility
122+
try:
123+
result = subprocess.run(['gcloud', 'config', 'get-value', 'project'],
124+
capture_output=True, text=True, check=True)
125+
project_id = result.stdout.strip()
126+
except subprocess.CalledProcessError:
127+
print("ERROR: Failed to get GCP project ID. Ensure gcloud is configured.")
128+
return
129+
130+
# Get environment variables
131+
openai_model = os.environ.get('OPENAI_MODEL_NAME', 'gpt-4o')
132+
openai_key = os.environ.get('OPENAI_API_KEY', 'Enter your API key')
133+
openai_base = os.environ.get('OPENAI_API_BASE', 'https://api.openai.com/v1')
134+
135+
# Build commands with actual values
119136
commands = [
120-
"yes | gcloud auth configure-docker us-central1-docker.pkg.dev",
121-
"gcloud artifacts repositories create praisonai-repository --repository-format=docker --location=us-central1",
122-
"docker build --platform linux/amd64 -t gcr.io/$(gcloud config get-value project)/praisonai-app:latest .",
123-
"docker tag gcr.io/$(gcloud config get-value project)/praisonai-app:latest us-central1-docker.pkg.dev/$(gcloud config get-value project)/praisonai-repository/praisonai-app:latest",
124-
"docker push us-central1-docker.pkg.dev/$(gcloud config get-value project)/praisonai-repository/praisonai-app:latest",
125-
"gcloud run deploy praisonai-service --image us-central1-docker.pkg.dev/$(gcloud config get-value project)/praisonai-repository/praisonai-app:latest --platform managed --region us-central1 --allow-unauthenticated --set-env-vars OPENAI_MODEL_NAME=${OPENAI_MODEL_NAME},OPENAI_API_KEY=${OPENAI_API_KEY},OPENAI_API_BASE=${OPENAI_API_BASE}"
137+
['gcloud', 'auth', 'configure-docker', 'us-central1-docker.pkg.dev'],
138+
['gcloud', 'artifacts', 'repositories', 'create', 'praisonai-repository',
139+
'--repository-format=docker', '--location=us-central1'],
140+
['docker', 'build', '--platform', 'linux/amd64', '-t',
141+
f'gcr.io/{project_id}/praisonai-app:latest', '.'],
142+
['docker', 'tag', f'gcr.io/{project_id}/praisonai-app:latest',
143+
f'us-central1-docker.pkg.dev/{project_id}/praisonai-repository/praisonai-app:latest'],
144+
['docker', 'push',
145+
f'us-central1-docker.pkg.dev/{project_id}/praisonai-repository/praisonai-app:latest'],
146+
['gcloud', 'run', 'deploy', 'praisonai-service',
147+
'--image', f'us-central1-docker.pkg.dev/{project_id}/praisonai-repository/praisonai-app:latest',
148+
'--platform', 'managed', '--region', 'us-central1', '--allow-unauthenticated',
149+
'--set-env-vars', f'OPENAI_MODEL_NAME={openai_model},OPENAI_API_KEY={openai_key},OPENAI_API_BASE={openai_base}']
126150
]
127-
128-
for cmd in commands:
151+
152+
# Run commands with appropriate handling for each platform
153+
for i, cmd in enumerate(commands):
129154
try:
130-
subprocess.run(cmd, shell=True, check=True)
155+
if i == 0: # First command (gcloud auth configure-docker)
156+
if platform.system() != 'Windows':
157+
# On Unix, pipe 'yes' to auto-confirm
158+
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
159+
proc.communicate(input=b'Y\n')
160+
if proc.returncode != 0:
161+
raise subprocess.CalledProcessError(proc.returncode, cmd)
162+
else:
163+
# On Windows, try with --quiet flag to avoid prompts
164+
cmd_with_quiet = cmd + ['--quiet']
165+
try:
166+
subprocess.run(cmd_with_quiet, check=True)
167+
except subprocess.CalledProcessError:
168+
# If --quiet fails, try without it
169+
print("Note: You may need to manually confirm the authentication prompt")
170+
subprocess.run(cmd, check=True)
171+
else:
172+
# Run other commands normally
173+
subprocess.run(cmd, check=True)
131174
except subprocess.CalledProcessError as e:
132-
print(f"ERROR: Command '{e.cmd}' failed with exit status {e.returncode}")
175+
print(f"ERROR: Command failed with exit status {e.returncode}")
176+
# Commands 2 (build) and 4 (push) and 5 (deploy) are critical
177+
if i in [2, 4, 5]:
178+
print("Critical command failed. Aborting deployment.")
179+
return
133180
print(f"Continuing with the next command...")
134181

135182
# Usage

0 commit comments

Comments
 (0)