Skip to content

Commit eae9879

Browse files
committed
feat: implement ms-agent ui command to run webui
1 parent 84f0c49 commit eae9879

File tree

7 files changed

+207
-6
lines changed

7 files changed

+207
-6
lines changed

MANIFEST.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ 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 *

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

requirements/webui.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
aiohttp
2+
fastapi
3+
pandas
4+
uvicorn

webui/backend/api.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,21 +103,43 @@ async def get_project_readme(project_id: str):
103103

104104

105105
@router.get('/projects/{project_id}/workflow')
106-
async def get_project_workflow(project_id: str):
107-
"""Get the workflow configuration for a project"""
106+
async def get_project_workflow(project_id: str,
107+
session_id: Optional[str] = None):
108+
"""Get the workflow configuration for a project
109+
110+
If session_id is provided, returns the workflow based on the session's workflow_type.
111+
For code_genesis project, 'simple' workflow_type will return simple_workflow.yaml.
112+
"""
108113
project = project_discovery.get_project(project_id)
109114
if not project:
110115
raise HTTPException(status_code=404, detail='Project not found')
111116

112-
workflow_file = os.path.join(project['path'], 'workflow.yaml')
117+
# Determine workflow_type from session if session_id is provided
118+
workflow_type = 'standard' # default
119+
if session_id:
120+
session = session_manager.get_session(session_id)
121+
if session and session.get('workflow_type'):
122+
workflow_type = session['workflow_type']
123+
124+
# Determine which workflow file to use
125+
if workflow_type == 'simple' and project.get('supports_workflow_switch'):
126+
# For simple workflow, try simple_workflow.yaml first
127+
workflow_file = os.path.join(project['path'], 'simple_workflow.yaml')
128+
if not os.path.exists(workflow_file):
129+
# Fallback to standard workflow.yaml if simple_workflow.yaml doesn't exist
130+
workflow_file = os.path.join(project['path'], 'workflow.yaml')
131+
else:
132+
# Standard workflow
133+
workflow_file = os.path.join(project['path'], 'workflow.yaml')
134+
113135
if not os.path.exists(workflow_file):
114136
raise HTTPException(status_code=404, detail='Workflow file not found')
115137

116138
try:
117139
import yaml
118140
with open(workflow_file, 'r', encoding='utf-8') as f:
119141
workflow_data = yaml.safe_load(f)
120-
return {'workflow': workflow_data}
142+
return {'workflow': workflow_data, 'workflow_type': workflow_type}
121143
except Exception as e:
122144
raise HTTPException(
123145
status_code=500, detail=f'Error reading workflow file: {str(e)}')

webui/frontend/src/components/ConversationView.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,9 @@ const ConversationView: React.FC<ConversationViewProps> = ({ showLogs }) => {
199199

200200
setWorkflowLoading(true);
201201
try {
202-
const response = await fetch(`/api/projects/${currentSession.project_id}/workflow`);
202+
// Include session_id in query params so backend can determine workflow_type
203+
const url = `/api/projects/${currentSession.project_id}/workflow${currentSession?.id ? `?session_id=${currentSession.id}` : ''}`;
204+
const response = await fetch(url);
203205
if (response.ok) {
204206
const data = await response.json();
205207
setWorkflowData(data.workflow || {});
@@ -650,7 +652,7 @@ const ConversationView: React.FC<ConversationViewProps> = ({ showLogs }) => {
650652
)}
651653

652654
{/* Loading Indicator - Shows current step in progress */}
653-
{!isStreaming && messages.length > 0 && (() => {
655+
{!isStreaming && messages.length > 0 && currentSession?.status === 'running' && (() => {
654656
// If waiting for input, don't show "in progress" indicator
655657
// The "Refine completed" message will be shown instead
656658
if (isWaitingForInput) {

webui/frontend/src/context/SessionContext.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export interface Session {
4040
workflow_progress?: WorkflowProgress;
4141
file_progress?: FileProgress;
4242
current_step?: string;
43+
workflow_type?: 'standard' | 'simple';
4344
}
4445

4546
export interface LogEntry {

0 commit comments

Comments
 (0)