Skip to content

Commit 8f0b0a3

Browse files
authored
Feat/webui code genesis (#837)
1 parent c6c8e41 commit 8f0b0a3

39 files changed

+2891
-612
lines changed

MANIFEST.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@ recursive-include ms_agent/tools/code_interpreter *.ttf
22
recursive-include ms_agent/utils *.tiktoken
33
recursive-include ms_agent/utils/nltk *.zip
44
recursive-include ms_agent/ *.yaml
5+
recursive-include webui/backend *.py
6+
recursive-include webui/frontend/dist *
57
recursive-include projects *

ms_agent/cli/cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from ms_agent.cli.app import AppCMD
44
from ms_agent.cli.run import RunCMD
5+
from ms_agent.cli.ui import UICMD
56

67

78
def run_cmd():
@@ -18,6 +19,7 @@ def run_cmd():
1819

1920
RunCMD.define_args(subparsers)
2021
AppCMD.define_args(subparsers)
22+
UICMD.define_args(subparsers)
2123

2224
# unknown args will be handled in config.py
2325
args, _ = parser.parse_known_args()

ms_agent/cli/ui.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# Copyright (c) ModelScope Contributors. All rights reserved.
2+
import argparse
3+
import os
4+
import sys
5+
import threading
6+
import time
7+
import webbrowser
8+
from pathlib import Path
9+
10+
from .base import CLICommand
11+
12+
13+
def subparser_func(args):
14+
""" Function which will be called for a specific sub parser.
15+
"""
16+
return UICMD(args)
17+
18+
19+
class UICMD(CLICommand):
20+
"""The webui command class."""
21+
22+
name = 'ui'
23+
24+
def __init__(self, args):
25+
self.args = args
26+
27+
@staticmethod
28+
def define_args(parsers: argparse.ArgumentParser):
29+
"""Define args for the ui command."""
30+
parser: argparse.ArgumentParser = parsers.add_parser(UICMD.name)
31+
parser.add_argument(
32+
'--host',
33+
type=str,
34+
default='0.0.0.0',
35+
help='The server host to bind to.')
36+
parser.add_argument(
37+
'--port',
38+
type=int,
39+
default=7860,
40+
help='The server port to bind to.')
41+
parser.add_argument(
42+
'--reload',
43+
action='store_true',
44+
help='Enable auto-reload for development.')
45+
parser.add_argument(
46+
'--production',
47+
action='store_true',
48+
help='Run in production mode (serve built frontend).')
49+
parser.add_argument(
50+
'--no-browser',
51+
action='store_true',
52+
help='Do not automatically open browser.')
53+
parser.set_defaults(func=subparser_func)
54+
55+
def execute(self):
56+
current_file = Path(__file__).resolve()
57+
project_root = current_file.parent.parent.parent.parent
58+
webui_dir = project_root / 'webui'
59+
60+
if not webui_dir.exists():
61+
import ms_agent
62+
ms_agent_path = Path(ms_agent.__file__).parent.parent
63+
webui_dir = ms_agent_path / 'webui'
64+
65+
if not webui_dir.exists():
66+
webui_dir = Path.cwd() / 'webui'
67+
68+
backend_dir = webui_dir / 'backend'
69+
frontend_dir = webui_dir / 'frontend'
70+
71+
if not webui_dir.exists() or not backend_dir.exists():
72+
print('Error: WebUI directory not found.')
73+
sys.exit(1)
74+
75+
frontend_dist = frontend_dir / 'dist'
76+
frontend_built = frontend_dist.exists() and (frontend_dist
77+
/ 'index.html').exists()
78+
79+
if self.args.production and not frontend_built:
80+
print(
81+
'Error: Frontend not built. Please run "npm run build" in webui/frontend first.'
82+
)
83+
sys.exit(1)
84+
85+
if not self.args.production and not frontend_built:
86+
if self._build_frontend(frontend_dir):
87+
frontend_built = True
88+
89+
browser_host = 'localhost' if self.args.host == '0.0.0.0' else self.args.host
90+
browser_url = f'http://{browser_host}:{self.args.port}'
91+
92+
backend_str = str(backend_dir)
93+
if backend_str not in sys.path:
94+
sys.path.insert(0, backend_str)
95+
96+
original_argv = sys.argv
97+
original_cwd = os.getcwd()
98+
try:
99+
os.chdir(backend_dir)
100+
from main import main
101+
102+
sys.argv = [
103+
'main.py',
104+
'--host',
105+
self.args.host,
106+
'--port',
107+
str(self.args.port),
108+
]
109+
if self.args.reload:
110+
sys.argv.append('--reload')
111+
112+
if not self.args.no_browser and frontend_built:
113+
114+
def open_browser():
115+
time.sleep(1.5)
116+
webbrowser.open(browser_url)
117+
118+
browser_thread = threading.Thread(
119+
target=open_browser, daemon=True)
120+
browser_thread.start()
121+
122+
main()
123+
except KeyboardInterrupt:
124+
print('\nShutting down...')
125+
sys.exit(0)
126+
except Exception as e:
127+
print(f'Error starting WebUI: {e}')
128+
import traceback
129+
traceback.print_exc()
130+
sys.exit(1)
131+
finally:
132+
sys.argv = original_argv
133+
os.chdir(original_cwd)
134+
135+
def _build_frontend(self, frontend_dir: Path) -> bool:
136+
import subprocess
137+
138+
try:
139+
subprocess.run(['npm', '--version'],
140+
capture_output=True,
141+
check=True,
142+
timeout=5)
143+
except (subprocess.TimeoutExpired, subprocess.CalledProcessError,
144+
FileNotFoundError):
145+
return False
146+
147+
node_modules = frontend_dir / 'node_modules'
148+
if not node_modules.exists():
149+
try:
150+
subprocess.run(['npm', 'install'],
151+
cwd=frontend_dir,
152+
check=True,
153+
timeout=300,
154+
stdout=subprocess.PIPE,
155+
stderr=subprocess.PIPE)
156+
except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
157+
return False
158+
159+
try:
160+
subprocess.run(['npm', 'run', 'build'],
161+
cwd=frontend_dir,
162+
check=True,
163+
timeout=300,
164+
stdout=subprocess.PIPE,
165+
stderr=subprocess.PIPE)
166+
return True
167+
except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
168+
return False

ms_agent/config/config.py

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) ModelScope Contributors. All rights reserved.
1+
# Copyright (c) Alibaba, Inc. and its affiliates.
22
import argparse
33
import os.path
44
from abc import abstractmethod
@@ -108,7 +108,10 @@ def fill_missing_fields(config: DictConfig) -> DictConfig:
108108
@staticmethod
109109
def is_workflow(config: DictConfig) -> bool:
110110
assert config.name is not None, 'Cannot find a valid name in this config'
111-
return config.name in ['workflow.yaml', 'workflow.yml']
111+
return config.name in [
112+
'workflow.yaml', 'workflow.yml', 'simple_workflow.yaml',
113+
'simple_workflow.yml'
114+
]
112115

113116
@staticmethod
114117
def parse_args() -> Dict[str, Any]:
@@ -124,15 +127,6 @@ def parse_args() -> Dict[str, Any]:
124127
_dict_config[key[2:]] = value
125128
return _dict_config
126129

127-
@staticmethod
128-
def safe_get_config(config: DictConfig, keys: str) -> Any:
129-
node = config
130-
for key in keys.split('.'):
131-
if not hasattr(node, key):
132-
return None
133-
node = getattr(node, key)
134-
return node
135-
136130
@staticmethod
137131
def _update_config(config: Union[DictConfig, ListConfig],
138132
extra: Dict[str, str] = None):
@@ -151,14 +145,42 @@ def traverse_config(_config: Union[DictConfig, ListConfig, Any],
151145
if current_path in extra:
152146
logger.info(
153147
f'Replacing {current_path} with extra value.')
154-
setattr(_config, name, extra[current_path])
148+
# Convert temperature to float and max_tokens to int if they're numeric strings
149+
value_to_set = extra[current_path]
150+
if name == 'temperature' and isinstance(
151+
value_to_set, str):
152+
try:
153+
value_to_set = float(value_to_set)
154+
except (ValueError, TypeError):
155+
pass
156+
elif name == 'max_tokens' and isinstance(
157+
value_to_set, str):
158+
try:
159+
value_to_set = int(value_to_set)
160+
except (ValueError, TypeError):
161+
pass
162+
setattr(_config, name, value_to_set)
155163
# Find the key in extra that matches name (case-insensitive)
156164
elif (key_match := next(
157165
(key
158166
for key in extra if key.lower() == name.lower()),
159167
None)) is not None:
160168
logger.info(f'Replacing {name} with extra value.')
161-
setattr(_config, name, extra[key_match])
169+
# Convert temperature to float and max_tokens to int if they're numeric strings
170+
value_to_set = extra[key_match]
171+
if name == 'temperature' and isinstance(
172+
value_to_set, str):
173+
try:
174+
value_to_set = float(value_to_set)
175+
except (ValueError, TypeError):
176+
pass
177+
elif name == 'max_tokens' and isinstance(
178+
value_to_set, str):
179+
try:
180+
value_to_set = int(value_to_set)
181+
except (ValueError, TypeError):
182+
pass
183+
setattr(_config, name, value_to_set)
162184
# Handle placeholder replacement like <api_key>
163185
elif (isinstance(value, str) and value.startswith('<')
164186
and value.endswith('>')

0 commit comments

Comments
 (0)