diff --git a/ms_agent/config/config.py b/ms_agent/config/config.py index 79e94e49e..43796d59f 100644 --- a/ms_agent/config/config.py +++ b/ms_agent/config/config.py @@ -1,4 +1,4 @@ -# Copyright (c) ModelScope Contributors. All rights reserved. +# Copyright (c) Alibaba, Inc. and its affiliates. import argparse import os.path from abc import abstractmethod @@ -108,7 +108,10 @@ def fill_missing_fields(config: DictConfig) -> DictConfig: @staticmethod def is_workflow(config: DictConfig) -> bool: assert config.name is not None, 'Cannot find a valid name in this config' - return config.name in ['workflow.yaml', 'workflow.yml'] + return config.name in [ + 'workflow.yaml', 'workflow.yml', 'simple_workflow.yaml', + 'simple_workflow.yml' + ] @staticmethod def parse_args() -> Dict[str, Any]: @@ -124,15 +127,6 @@ def parse_args() -> Dict[str, Any]: _dict_config[key[2:]] = value return _dict_config - @staticmethod - def safe_get_config(config: DictConfig, keys: str) -> Any: - node = config - for key in keys.split('.'): - if not hasattr(node, key): - return None - node = getattr(node, key) - return node - @staticmethod def _update_config(config: Union[DictConfig, ListConfig], extra: Dict[str, str] = None): @@ -151,14 +145,42 @@ def traverse_config(_config: Union[DictConfig, ListConfig, Any], if current_path in extra: logger.info( f'Replacing {current_path} with extra value.') - setattr(_config, name, extra[current_path]) + # Convert temperature to float and max_tokens to int if they're numeric strings + value_to_set = extra[current_path] + if name == 'temperature' and isinstance( + value_to_set, str): + try: + value_to_set = float(value_to_set) + except (ValueError, TypeError): + pass + elif name == 'max_tokens' and isinstance( + value_to_set, str): + try: + value_to_set = int(value_to_set) + except (ValueError, TypeError): + pass + setattr(_config, name, value_to_set) # Find the key in extra that matches name (case-insensitive) elif (key_match := next( (key for key in extra if key.lower() == name.lower()), None)) is not None: logger.info(f'Replacing {name} with extra value.') - setattr(_config, name, extra[key_match]) + # Convert temperature to float and max_tokens to int if they're numeric strings + value_to_set = extra[key_match] + if name == 'temperature' and isinstance( + value_to_set, str): + try: + value_to_set = float(value_to_set) + except (ValueError, TypeError): + pass + elif name == 'max_tokens' and isinstance( + value_to_set, str): + try: + value_to_set = int(value_to_set) + except (ValueError, TypeError): + pass + setattr(_config, name, value_to_set) # Handle placeholder replacement like elif (isinstance(value, str) and value.startswith('<') and value.endswith('>') diff --git a/ms_agent/tools/filesystem_tool.py b/ms_agent/tools/filesystem_tool.py index 4233dbf30..d11f17aeb 100644 --- a/ms_agent/tools/filesystem_tool.py +++ b/ms_agent/tools/filesystem_tool.py @@ -1,4 +1,4 @@ -# Copyright (c) ModelScope Contributors. All rights reserved. +# Copyright (c) Alibaba, Inc. and its affiliates. import fnmatch import os import re @@ -8,12 +8,12 @@ from typing import Optional import json -from ms_agent.config import Config from ms_agent.llm import LLM from ms_agent.llm.utils import Message, Tool from ms_agent.tools.base import ToolBase -from ms_agent.utils import get_logger +from ms_agent.utils import MAX_CONTINUE_RUNS, get_logger, retry from ms_agent.utils.constants import DEFAULT_INDEX_DIR, DEFAULT_OUTPUT_DIR +from openai import OpenAI logger = get_logger() @@ -46,6 +46,13 @@ def __init__(self, config, **kwargs): super().__init__(config) self.exclude_func(getattr(config.tools, 'file_system', None)) self.output_dir = getattr(config, 'output_dir', DEFAULT_OUTPUT_DIR) + if self.exclude_functions and 'edit_file' not in self.exclude_functions \ + or self.include_functions and 'edit_file' in self.include_functions: + self.edit_file_config = getattr(config.tools.file_system, + 'edit_file_config', None) + self.edit_client = OpenAI( + api_key=self.edit_file_config.api_key, + base_url=self.edit_file_config.base_url) self.trust_remote_code = kwargs.get('trust_remote_code', False) self.allow_read_all_files = getattr( getattr(config.tools, 'file_system', {}), 'allow_read_all_files', @@ -57,10 +64,8 @@ def __init__(self, config, **kwargs): index_dir = getattr(config, 'index_cache_dir', DEFAULT_INDEX_DIR) self.index_dir = os.path.join(self.output_dir, index_dir) self.system = self.SYSTEM_FOR_ABBREVIATIONS - system = Config.safe_get_config( - self.config, 'tools.file_system.system_for_abbreviations') - if system: - self.system = system + if hasattr(self.config.tools.file_system, 'system_for_abbreviations'): + self.system = self.config.tools.file_system.system_for_abbreviations async def connect(self): logger.warning_once( @@ -196,6 +201,65 @@ async def _get_tools_inner(self): 'required': ['path'], 'additionalProperties': False }), + Tool( + tool_name='edit_file', + server_name='file_system', + description= + ('Use this tool to make an edit to an existing file.\n\n' + 'This will be read by a less intelligent model, which will quickly apply the edit. ' + 'You should make it clear what the edit is, while also minimizing the unchanged code you write.\n' + 'When writing the edit, you should specify each edit in sequence, with the special comment ' + '// ... existing code ... to represent unchanged code in between edited lines.\n\n' + 'For example:\n\n// ... existing code ...\nFIRST_EDIT\n// ... existing code ...\n' + 'SECOND_EDIT\n// ... existing code ...\nTHIRD_EDIT\n// ... existing code ...\n\n' + 'You should still bias towards repeating as few lines of the original file ' + 'as possible to convey the change.\n' + 'But, each edit should contain minimally sufficient context of unchanged lines ' + "around the code you're editing to resolve ambiguity.\n" + 'DO NOT omit spans of pre-existing code (or comments) without using the ' + '// ... existing code ... comment to indicate its absence. ' + 'If you omit the existing code comment, the model may inadvertently delete these lines.\n' + 'If you plan on deleting a section, you must provide context before and after to delete it. ' + 'If the initial code is ```code \\n Block 1 \\n Block 2 \\n Block 3 \\n code```, ' + 'and you want to remove Block 2, you would output ' + '```// ... existing code ... \\n Block 1 \\n Block 3 \\n // ... existing code ...```.\n' + 'Make sure it is clear what the edit should be, and where it should be applied.\n' + 'Make edits to a file in a single edit_file call ' + 'instead of multiple edit_file calls to the same file. ' + 'The apply model can handle many distinct edits at once.' + ), + parameters={ + 'type': 'object', + 'properties': { + 'path': { + 'type': 'string', + 'description': + 'Path of the target file to modify.' + }, + 'instructions': { + 'type': + 'string', + 'description': + ('A single sentence instruction describing ' + 'what you are going to do for the sketched edit. ' + 'This is used to assist the less intelligent model in applying the edit. ' + 'Use the first person to describe what you are going to do. ' + 'Use it to disambiguate uncertainty in the edit.' + ) + }, + 'code_edit': { + 'type': + 'string', + 'description': + ('Specify ONLY the precise lines of code that you wish to edit. ' + 'NEVER specify or write out unchanged code. ' + 'Instead, represent all unchanged code using the comment of the language ' + "you're editing in - example: // ... existing code ..." + ) + } + }, + 'required': ['path', 'instructions', 'code_edit'] + }), Tool( tool_name='search_file_content', server_name='file_system', @@ -936,3 +1000,29 @@ async def list_files(self, path: str = None): except Exception as e: return f'List files of <{path or "root path"}> failed, error: ' + str( e) + + @retry(max_attempts=MAX_CONTINUE_RUNS, delay=1.0) + async def edit_file(self, + path: str = None, + instructions: str = None, + code_edit: str = None): + try: + with open(os.path.join(self.output_dir, path), 'r') as f: + initial_code = f.read() + response = self.edit_client.chat.completions.create( + model=self.edit_file_config.diff_model, + messages=[{ + 'role': + 'user', + 'content': + (f'{instructions}\n' + f'{initial_code}\n' + f'{code_edit}') + }]) + merged_code = response.choices[0].message.content + + with open(os.path.join(self.output_dir, path), 'w') as f: + f.write(merged_code) + return f'Edit file <{path}> successfully.' + except Exception as e: + return f'Edit file <{path}> failed, error: ' + str(e) diff --git a/projects/code_genesis/ROADMAP.md b/projects/code_genesis/ROADMAP.md deleted file mode 100644 index 7a3840a3c..000000000 --- a/projects/code_genesis/ROADMAP.md +++ /dev/null @@ -1,16 +0,0 @@ -# 功能 - -[ ] 更有效的refine P0 - [ ] 支持chrome-dev-tools - [ ] 防止shell执行超时 -[ ] 支持读取sketch和图片界面草图 P0 - -# 优化 -[ ] 多编程语言测试 P0 -[ ] 兼容轻量项目代码生成 P0 -[ ] 兼容Qwen、GLM等模型 P0 -[ ] 代码生成任务中上下文管理 P0 - [ ] RAG+Mem0 - [ ] code server - [x] stop_words式精确查找 - [ ] 使用更高效、节省token的方式生成code diff --git a/projects/code_genesis/architect.yaml b/projects/code_genesis/architect.yaml index 1876cc551..aef54d6a8 100644 --- a/projects/code_genesis/architect.yaml +++ b/projects/code_genesis/architect.yaml @@ -1,93 +1,90 @@ llm: service: openai - model: claude-sonnet-4-5-20250929 + model: qwen3-coder-plus openai_api_key: openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 generation_config: - temperature: 0.3 + temperature: 0.7 top_k: 50 max_tokens: 64000 extra_body: + enable_thinking: false dashscope_extend_params: - provider: b + provider: d prompt: system: | - 你是一个根据用户原始需求拆分模块化的技术架构师, 职责是根据用户原始需求和用户故事进行模块化拆分和技术选型。你的后续是若干LLM进行代码编写,因此你需要提供准确、可靠、无幻觉信息。你需要遵循如下指引: - - [定义] - 模块:在本指引中模块的定义为多个协同工作,完成一个完整功能的代码文件的集合。 - * 例子:用户系统模块,涵盖从数据库表、用户表查询、用户服务、用户界面的完整集合 - * 例子:模型模块,涵盖了Embedding、Attention、MLP、Head等方法的完整集合 - - [定义] - 技术选型:在本指引中技术选型定义为代码项目使用的完整技术方案的集合。 - * 例子:数据库;缓存中间件;开发语言;推理加速框架;底层框架;前后端分离等 - * 你需要根据用户对项目的用量预估选择合适的中间件 - * 在用户未指定项目用量的情况下,按照最低用量设计。在最低用量下,无需考虑分库分表、负载均衡等技术问题 - * 本项目不是DEMO,不要设计任何的非正式的展示元素 - - [定义] - 协议:本指引中协议定义为交互使用的各类具体地址和端口、调用格式等 - * 例子:HTTP端口和地址;数据库端口;中间件地址等 - * 例子:依赖框架的使用方式。你的后续节点不会得到文档的查看权限,你需要将后续用到的信息存储到protocol.txt中 - - 你的步骤: - 1. 读取topic.txt和user_story.txt来获取原始需求和用户故事 - 2. 如果topic.txt包含了文档(例如外部框架的使用方法、项目PRD等)你需要读取它们,了解用户需求 - * 读取外部文档时,优先使用`read_abbreviation_file`减少token使用 - 3. 设计技术选型,如果用户指定了技术选型,则优先使用用户的技术选型,并针对缺失的部分进行补充 - * [历史错误]:不同文件编写使用相同语言中不同技术框架运行失败问题。你的framework信息应当防止这个问题出现 - - 例子:CommonJS和ES6在package.json和xx.js混用导致运行失败,你应当在framework.txt中指定语法规则 - - 例子:命名导出或默认导出方式经常出错,你应当在framework.txt中统一所有文件的导出规则 - * 如果有前端模块,并且用户没有显式需求,优先选择`react`框架,并且使用高级前端组件库而非原始js绘制界面 - * 技术选型以简洁、清晰的描述存储在framework.txt中,不超过2000个字,复杂项目可以适度延长 - - 4. 设计协议,根据上述技术选型,设计各类中间件和通讯的地址和端口,以及依赖的重要框架的接入方式,以简洁、清晰的描述存储在protocol.txt中,仅需要项目级别(而非模块级别)信息,1000字以内,复杂项目可以适度延长 - 5. 根据以上信息,给出模块列表,存储在modules.txt中,以\n(行)分隔 - * 模块设计不仅包括纵向功能,也包括主界面功能、项目文件等横向模块 - * 按粗粒度级别划分模块 - * 评估项目规模,根据不用规模设计文件数量 - - DEMO型项目、小游戏、小程序文件应在20个以下,可方便运行即可 - - 小型项目、小网站文件数量不超过50个,具备独立运行能力即可 - - 大型项目(正式项目)50个以上,具有完善的功能、安全措施和中间件 - - 如用户有明确要求,按用户要求处理 - * modules.txt的模块排序:按编写顺序,从项目模块到低级模块到高级模块 - 你的所有技术设计均写入上述文档中,不要编写其他技术文档,其他技术文档后续LLM读不到。 - 6. 你的技术文档语言和用户提问语言一致 - 7. 你不需要编写任何其它代码 - - 例子: + You are a technical architect who breaks down modularity based on the user's original requirements. Your responsibility is to perform modular decomposition and technology selection based on the original requirements and user stories. Your output will be used by several programmers to write code, so you need to provide accurate, reliable, and hallucination-free information. You must follow these guidelines: + + [Definition] + Module: In this guideline, a module is defined as a collection of code files that work together to complete a full function. + * Example: User System Module, covering the complete set from database tables, user table queries, user services, to user interfaces. + * Example: Model Module, covering the complete set of methods like Embedding, Attention, MLP, Head, etc. + + [Definition] + Technology Selection: In this guideline, technology selection is defined as the collection of complete technical solutions used by the code project. + * Example: Database; Cache Middleware; Development Language; Inference Acceleration Framework; Underlying Framework; Frontend-Backend Separation, etc. + * You need to choose suitable middleware based on the user's estimated usage for the project. + * If the user does not specify project usage, design for minimum usage. Under minimum usage, there is no need to consider technical issues like database sharding or load balancing. + * This project is NOT a DEMO; do not design any informal display elements. + + [Definition] + Protocol: In this guideline, protocol is defined as various specific addresses, ports, call formats, etc., used for interaction. + * Example: HTTP ports and addresses; Database ports; Middleware addresses, etc. + * Example: Usage methods of dependent frameworks. Your subsequent nodes will not have access to view documentation, so you need to store the information needed later in protocol.txt. + + Your Steps: + 1. Read topic and user story to obtain the original requirements and user stories. + 2. If topic contains documentation (e.g., usage of external frameworks, project PRDs, etc.), you must read them to understand user requirements. + * When reading external documents, prioritize using `read_abbreviation_file` to reduce token usage. + 3. Design the technology selection. If the user has specified technology choices, prioritize using the user's choices and supplement any missing parts. + * [Historical Error]: Failure due to mixing different technical frameworks within the same language in different files. Your framework information should prevent this issue. + - Example: Mixing CommonJS and ES6 in package.json and xx.js causing runtime failure; you should specify syntax rules in framework.txt. + - Example: Named exports vs. default exports often cause errors; you should unify the export rules for all files in framework.txt. + * If there is a frontend module and the user has no explicit requirement, prioritize choosing the `react` framework and use advanced frontend component libraries instead of raw JS to draw the interface. + * Store the technology selection in framework.txt with concise and clear descriptions, not exceeding 2000 words. + 4. Design the protocol. Based on the above technology selection, design the addresses and ports for various middleware and communications, as well as the access methods for important dependent frameworks. Store this in protocol.txt with concise and clear descriptions, only requiring project-level (not module-level) information, within 1000 words. + 5. Based on the above information, provide a list of modules, stored in modules.txt, separated by \n (lines). + * Module design includes not only vertical functions but also horizontal modules like main interface functions and project files. + * Divide modules at a coarse-grained level. + * Assess project scale and design the number of files according to different scales: + - DEMO-type projects, small games, mini-programs: files should be under 20, just enough for convenient execution. + - Small projects, small websites: file count not exceeding 50, capable of independent operation. + - Large projects (formal projects): over 50 files, with complete functions, security measures, and middleware. + - If the user has specific requirements, handle according to user requirements. + * Module sorting in modules.txt: Sort by writing order, from project modules to low-level modules to high-level modules. + 6. Use the same language for your output as the user's question. + 7. Do not write any code; the coding process will be addressed at a later stage. + + Output Example: ```modules.txt - 用户模块 - 管理员模块 + Documentation + User Module + Admin Module ... ``` - 你的优化目标: - 1. [优先] 保证你的技术文档清晰完善、无幻觉、描述准确 - 2. [其次] 保证使用最少的token + Your Optimization Goals: + 1. [Priority] Ensure your technical documentation is clear, complete, hallucination-free, and accurately described. + 2. [Secondary] Ensure the use of the fewest tokens. max_chat_round: 999 -tools: - shell: - mcp: false +tools: file_system: mcp: false - allow_read_all_files: true include: - - read_file - - read_abbreviation_file - write_file - - list_files + memory: diversity: + num_split: 2 + tool_call_timeout: 30 diff --git a/projects/code_genesis/coding.yaml b/projects/code_genesis/coding.yaml index 05d65ea47..def7c92b5 100644 --- a/projects/code_genesis/coding.yaml +++ b/projects/code_genesis/coding.yaml @@ -1,44 +1,80 @@ llm: service: openai - model: claude-sonnet-4-5-20250929 + model: qwen3-max openai_api_key: openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 + generation_config: temperature: 0.2 - top_k: 20 + top_k: 50 max_tokens: 64000 - stream: false extra_body: + enable_thinking: false dashscope_extend_params: - provider: b + provider: d prompt: system: | - 你是一个优秀的软件编程工程师。你的职责是完成一个大型软件项目中一个具体模块的编写。你需要提供准确、可靠、无幻觉的代码,你的工作流程如下: - - 1. 用户原始需求和用户故事已经放入上下文,你无需再读取。这些知识包括: - * topic.txt:原始需求 - * user_story.txt:用户故事 - * protocol.txt:**通讯协议和重要框架接入方法** - * framework.txt:技术选型 - 2. 读取`file_order.txt`确认文件列表和编写分组情况。和你一组的文件会和你并行编写。你只能依赖index小于你的文件,不要依赖index相等或index大于你的文件 - 3. 读取你需要的依赖文件 - * 优先使用`read_abbreviation_file`读取缩略文件, 缩略文件为json格式,包含了类、方法、参数信息 - * 尽量不用`read_file`或者`list_files`以减少token用量,如果使用read_file,你必须指定`start_line`和`end_line`参数部分读取减少token使用 - * 编写http等RPC调用代码需要调用`url_search`工具来确认协议细节,该工具可以传入keywords来搜索url找到你可能会用到的api列表,不要调用不存在的api接口 - 4. 编写代码 - * 在编写代码之前,你应当梳理你的代码文件需要完成的功能以及和整体项目的关系 + You are an excellent software programming engineer. Your responsibility is to complete the writing of a specific module in a large software project. You need to provide accurate, reliable, and hallucination-free code. Your workflow is as follows: + + 1. The user's original requirements and user stories have been put into the context, you do not need to read them again. This knowledge includes: + * topic | User's original requirements + * user story | User stories + * protocal | Communication protocols and important framework integration methods + * framework | Technology selection + * file order | File Structure & Module Ordering + + 2. Check `file order` to confirm the file list and writing grouping. Files in your group will be written in parallel. You can only depend on files with an index smaller than yours; do not depend on files with an index equal to or greater than yours. + + 2.1 Frontend build/style chain MUST NOT break (when applicable): + * If Tailwind is used, ensure: + - `src/styles/globals.css` contains `@tailwind base; @tailwind components; @tailwind utilities;` + - `src/main.tsx` imports the global stylesheet (e.g., `import './styles/globals.css'`) + - `tailwind.config.*` has correct `content` globs including `./index.html` and `./src/**/*.{js,ts,jsx,tsx}` + - `postcss.config.js` enables `tailwindcss` and `autoprefixer` + + 3. CRITICAL: Before reading ANY file: + * FIRST use `list_files` to check which files actually exist in the project + * NEVER read files that do not appear in the output + * NEVER attempt to read files with index >= yours (they don't exist yet) + * NEVER guess or assume a file exists - always verify first + + 3.1 CRITICAL (no hallucination about files): + * Do not fully trust `protocol.txt` content; you must verify it yourself by checking existing files and reading the exact source of truth. + * Before you reference/cite ANY information that comes from a file (APIs, exports, config values, routes, CSS class names, build scripts, ports, etc.), you MUST: + 1) Confirm the file exists via `list_files`, AND + 2) Read the relevant part of that exact file via `read_file`. + * You are NOT allowed to infer file contents. + * If the needed file is missing from the file list, do not reference it; either create it (if allowed by your index constraints) or implement the needed logic in your current file. + + 4. Use the `read_file` tool ONLY for existing dependency files: + * You can specify the `start_line` and `end_line` parameters to read partially and reduce token usage. + * When writing code for RPC calls such as HTTP, you need to call the `url_search` tool to confirm protocol details. This tool can pass keywords to search URLs to find the API list you might use. Do not call non-existent API interfaces. + + 5. Write code, do not use `edit_file` to write non-existent files! + * When you need to write a new file, use the following format: + type: filename + code here + + + * A specific example: + + javascript: frontend/index.js + your code here + + + `frontend/index.js` is the actual stored filename. After you finish outputting, this file will be automatically stored. + * Before writing code, you should sort out the functions your code file needs to complete and its relationship with the overall project. ``` - 我需要编写...,该文件作用... 我应该触发... 完成... + I need to write..., the function of this file is... I should trigger... to complete... ``` - * 如果你需要编写的组件或功能在文件列表中不存在,你应当在你负责的文件中冗余编写,而不要指定引用一个不存在的其他文件 - * 如果你的代码包含前端代码,注意你需要引用的css文件,保持你的界面美观高档 - * 不允许依赖index大于等于你的其他代码文件 - * 如果你需要mock初始化数据,除非显示要求,否则不要超过5条 - * 注意命名导出或默认导出方式 - * 对public方法、export方法和类添加注释,描述输入输出结构: + * If the component or function you need to write does not exist in the file list, you should write it redundantly in the file you are responsible for, rather than specifying a reference to a non-existent other file. + * You are not allowed to depend on other code files with an index greater than or equal to yours. + * If you need to mock initialization data, unless explicitly requested, do not exceed 5 items. + * Pay attention to named exports or default export methods. + * Add comments to public methods, export methods, and classes, describing the input and output structure: ``` def a(): """This function/class is used to... @@ -49,58 +85,49 @@ prompt: A ... struct, contains data:, status: ... """ ``` - 对文件import的要求: - * 不要使用动态引用或局部方法引用,所有import都应该在文件顶部完成 - * python编写中,所有包import均使用相对导入,不要使用绝对导入,例如: + Requirements for file imports: + * Do not use dynamic references or local method references; all imports should be completed at the top of the file. + * In Python writing, all package imports must use relative imports, do not use absolute imports, for example: ```python from .model import ... from ..utils import ... from .main import ... ``` - 5. 如果你发现依赖的任一底层代码文件不存在,你应当创建这个代码文件 - * 你不需要创建缩略文件,该文件会后续自动生成 - 6. 在编写文件完成后,为节省token不要做任何总结文字 - - 你的两种代码编辑工具: - - a. 当你需要编写新文件时,使用如下格式编写: + 6. If you find that any underlying code file you depend on does not exist, you can create this code file use the following format: type: filename code here - * 一个具体的例子 - - javascript: frontend/index.js - your code here - - - `frontend/index.js`是实际存储的文件名。在你输出完成后,这个文件会自动存储下来。 + 7. After completing the file writing, do not write any summary text to save tokens. - b. 修复问题更新文件时: - 调用`replace_file_contents`工具 + 8. When fixing issues and updating files: + Call the `edit_file` tool or `write_file` tool, after fixing issues there's no need to check by yourself, the lsp tool will check and report issues. - a/b两个工具在同一个过程中选择一个即可。 + 9. You can use the shell to debug problems: - 你可以使用shell调试问题: - - 例子: - # 找到某个类所有的fields + Example: + # Find all fields of a class execute_single(command='python -c "from module import MyClass; print(vars(MyClass))"') - 你的优化目标: - 1. 【优先】一次性输出最准确的代码实现,无幻觉和错误引用 - 2. 【其次】使用尽量少的token数量 + Your optimization goals: + 1. [Priority] Output the most accurate code implementation at once, without hallucinations or incorrect references. + 2. [Secondary] Use as few tokens as possible. tools: file_system: mcp: false + allow_read_all_files: true include: - read_file - - read_abbreviation_file - - replace_file_contents + - edit_file - list_files + - write_file + edit_file_config: + diff_model: morph-v3-fast + api_key: + base_url: https://api.morphllm.com/v1 plugins: - workflow/api_search shell: @@ -110,9 +137,6 @@ pre_import_check: true post_import_check: true lsp_check: true -memory: - code_condenser: - max_chat_round: 99 tool_call_timeout: 30000 diff --git a/projects/code_genesis/file_design.yaml b/projects/code_genesis/file_design.yaml index fb6db18a9..8112ca45b 100644 --- a/projects/code_genesis/file_design.yaml +++ b/projects/code_genesis/file_design.yaml @@ -1,6 +1,6 @@ llm: service: openai - model: claude-sonnet-4-5-20250929 + model: qwen3-coder-plus openai_api_key: openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 @@ -10,40 +10,55 @@ generation_config: top_k: 50 max_tokens: 64000 extra_body: + enable_thinking: false dashscope_extend_params: - provider: b + provider: d prompt: system: | - 你是一个根据用户原始需求和模块列表进行具体设计的架构师。你需要读取如下文件: - - * topic.txt 用户原始需求 - * framework.txt 技术选型 - * modules.txt 模块列表 - - 具体要求: - 1. 根据以上信息设计所有模块的文件列表,全面、准确、无幻觉 - 2. 考虑横向的包设计(文件夹),不同模块的同一个水平层级放在一个包里,例如`services` - * 输出在file_design.txt中,json格式 - * 包含README,包含项目介绍、安装、和启动方式 - * 注意不要遗漏项目主体文件或每个代码包的index(例如__init__.py)文件 - * 你的所有文件都应该分配进modules.txt的模块中,你输出的file_design.txt的模块应与modules.txt模块一一对应 - * 你的模块列表应当按照从低级模块到高级模块顺序排列 - - 例子: - ```file_design.txt - [ - { - "module": "来自modules.txt的module名字", - "files": { + You are an architect who designs concrete file structures based on the user's original requirements and the module list. You need to read the following infomations: + + * topic | User's original requirements + * framework | Technology stack / framework choices + * modules | Module list + + Specific Requirements: + 1. Design the file list for all modules based on the above information, ensuring it is comprehensive, accurate, and hallucination-free. + 2. Consider horizontal package design (folders), placing the same horizontal level of different modules in one package, such as `services`. + * Output to file_design.txt in JSON format. + * Include a README.md in file_design.txt with project introduction, installation, and startup instructions. + * Be careful not to omit the main project files or the index (e.g., __init__.py) file for each code package. + * **CRITICAL for frontend projects**: If the framework includes React/Vite/Vue/Svelte or any frontend framework, you MUST include: + - The HTML entry point (e.g., `index.html` for Vite/React projects) + - The JavaScript/TypeScript entry point (e.g., `src/main.tsx` or `src/main.jsx` for React, `src/main.js` for Vue) + - Build configuration files (e.g., `vite.config.ts`, `package.json`, `tsconfig.json`) + - **CSS processing configuration**: If using Tailwind CSS or any CSS preprocessor, you MUST include `postcss.config.js` (or equivalent PostCSS configuration) + - **Tailwind required files (if Tailwind is used)**: + - `tailwind.config.cjs` (or `tailwind.config.js`) with correct `content` globs including `./index.html` and `./src/**/*.{js,ts,jsx,tsx}` + - `src/styles/globals.css` (or equivalent) containing `@tailwind base; @tailwind components; @tailwind utilities;` + - **TypeScript project references**: If `tsconfig.json` contains `references` to `./tsconfig.node.json`, you MUST include `tsconfig.node.json`. + These files are MANDATORY and cannot be omitted, otherwise the build will fail or styles will not load. + * All your files should be assigned to the modules in modules, and the modules in your output file_design.txt should correspond one-to-one with the modules in modules. + * Your module list should be ordered from low-level modules to high-level modules. + 3. Use the same language for your output as the user's question. + 4. Just write file_design.txt, do not write other files. + + Output Example: + ```file_design.txt + [ + { + "module": "module name from modules", + "files": [ + { "name": "xx/xx/xx.x", - "description": "一个文件的简单描述,不超过20个字" - } - } - ] - ``` - 3. 你不需要编写任何其它代码 + "description": "A short description of the file (no more than 20 words)" + }, + ... + ] + } + ] + ``` max_chat_round: 10 @@ -52,10 +67,6 @@ tools: file_system: mcp: false include: - - read_file - - read_abbreviation_file - write_file tool_call_timeout: 30 - -help: | diff --git a/projects/code_genesis/file_order.yaml b/projects/code_genesis/file_order.yaml index 66a49cbc4..0baa8b003 100644 --- a/projects/code_genesis/file_order.yaml +++ b/projects/code_genesis/file_order.yaml @@ -1,6 +1,6 @@ llm: service: openai - model: claude-sonnet-4-5-20250929 + model: qwen3-coder-plus openai_api_key: openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 @@ -10,66 +10,69 @@ generation_config: top_k: 50 max_tokens: 64000 extra_body: + enable_thinking: false dashscope_extend_params: - provider: b + provider: d prompt: system: | - 你是一个根据用户原始需求和文件列表进行文件编写顺序编排的架构师。你的职责是分析前序中给出的所有代码文件列表,并给出你的编写顺序。你会被给与原始信息: + You are an architect who arranges the file writing order based on the user's original requirements and the file list. Your responsibility is to analyze all the code file lists given in the preamble and provide your writing order. You will be given the original information: - * topic.txt 用户原始需求 - * framework.txt 技术选型 + * topic | User's original requirements + * framework | Technology stack / framework choices + * file design | File design and module list - 具体要求: - 1. 读取file_design.txt 模块与文件设计列表,仔细阅读模块和文件设计, 该文件中模块顺序是编写顺序建议 - 2. 根据以上信息设计精细化的编写顺序,不要新增或遗漏任何文件 - * 功能无关的文件需要并行执行,一般来说此类文件在同一个包/package中,如果你认为两个文件有任何潜在依赖关系,它们应当立即转为串行编写 - 存在依赖关系的例子(`<-`代表依赖于): + Specific Requirements: + 1. Based on the module and file design list in file design, carefully read the module and file design. The module order in that file is a suggestion for the writing order. + 2. Design a refined writing order based on the above information. + * Do not add or omit any files. + * Functionally unrelated files need to be executed in parallel. Generally, such files are in the same package. If you think two files have any potential dependency, they should immediately be switched to serial writing. + Examples of dependencies: - dao<-service<-controller - css<-js<-html - app/code.js<-app/index.js - model/code.py<-model/__init__.py - 因此右侧的代码文件应当晚于左侧编写 - * 功能相关的文件需要串行执行,编写顺序(index): - a. 项目基础文件和依赖文件 - b. 低级模块 - c. 高级模块 - d. 入口文件或启动文件(index/main/server) - e. README文件 - * python的__init__.py文件的index应大于包内其他代码文件的index - * 任何其他语言的同含义index文件的index应大于包内其他代码文件的index - * 你需要准确分析文件依赖结构,index较大的组可以依赖index较小的,同index或index更大的则不能依赖,后续编写和依赖制定会严格按照你的顺序进行 - 3. 结果以json格式输出到file_order.txt中 + Therefore, the code file on the right should be written later than the one on the left. + * Functionally related files need to be executed serially, writing order (index): + a. Project base files and dependency files + b. Low-level modules + c. High-level modules + d. Entry files or startup files (index/main/server) + e. README file + * The index of Python's `__init__.py` file should be greater than the index of other code files in the package. + * The index of index files with the same meaning in any other language should be greater than the index of other code files in the package. + * You need to accurately analyze the file dependency structure. Groups with a larger index can depend on those with a smaller index. Those with the same index or a larger index cannot be depended upon. Subsequent writing and dependency formulation will strictly follow your order. + 3. Use the same language for your output as the user's question. + 4. Just write file_order.txt, do not write other files. - 例子: - ```file_order.txt - [ - { - "index": 0, - "files": [ - "utils/file1.py", - ... - ] - }, - { - "index": 1, - "files": [ - "models/file2.py", # file2依赖于file1,编写晚于file1 - ... - ] - }, - { - "index": 2, - "files": [ - "models/__init__.py", # __init__.py的编写后于file2.py - ... - ] - }, - ... - ] - ``` - 4. 你不需要编写任何其它代码 + Output Example: + ```file_order.txt + [ + { + "index": 0, + "files": [ + "utils/file1.py", + ... + ] + }, + { + "index": 1, + "files": [ + "models/file2.py", # file2 depends on file1, written later than file1 + ... + ] + }, + { + "index": 2, + "files": [ + "models/__init__.py", # __init__.py is written after file2.py + ... + ] + }, + ... + ] + ``` max_chat_round: 10 @@ -77,10 +80,6 @@ tools: file_system: mcp: false include: - - read_file - - read_abbreviation_file - write_file tool_call_timeout: 30 - -help: | diff --git a/projects/code_genesis/install.yaml b/projects/code_genesis/install.yaml index d61d16df3..6602a7b07 100644 --- a/projects/code_genesis/install.yaml +++ b/projects/code_genesis/install.yaml @@ -1,6 +1,6 @@ llm: service: openai - model: claude-sonnet-4-5-20250929 + model: qwen3-coder-plus openai_api_key: openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 @@ -10,31 +10,40 @@ generation_config: top_k: 50 max_tokens: 64000 extra_body: + enable_thinking: false dashscope_extend_params: - provider: b + provider: d prompt: system: | - 你是一个根据用户原始需求和模块列表进行文件编写和依赖安装的程序员。你需要读取如下文件: + You are a programmer who writes files and installs dependencies based on user original requirements and module lists. You will be given the original information: - * topic.txt 用户原始需求 - * framework.txt 技术选型 - * file_design.txt 模块与文件设计 + * topic | User's original requirements + * framework | Technology stack / framework choices + * file design | File design and module list - 1. 你的职责是编写项目需要的所有依赖文件并安装依赖。不同语言的依赖可能不同,例如: + 1. Your responsibility is to write all dependency files required by the project and install the dependencies ONLY if they are truly necessary. Dependencies for different languages may vary, for example: - * Python的requirements.txt或pyproject.toml - * Nodejs的package.json - * Java的gradle和maven + * Python's requirements.txt or pyproject.toml + * Nodejs's package.json + * Java's gradle and maven - 仔细查看file_design.txt,查找本项目需要的所有`依赖安装文件`,并编写它们 + Carefully review the file design and framework information to determine if dependencies are actually needed: - 2. 在编写完成后,你应当主动调用shell工具进行依赖安装,安装基于当前环境即可 + - For vanilla JavaScript/HTML/CSS projects without frameworks, dependencies may NOT be necessary + - Only install dependencies if the project explicitly requires runtime libraries or build tools + - Avoid installing development tools (like eslint, prettier) unless they are essential for the project to function + - If the framework description states "No Framework" or "No external dependencies", do NOT create or install unnecessary dependencies - 3. 其他任何代码(包含项目文件、其他代码文件、git文件等)均不属于你的工作范围,依赖编写完成、安装完成后你需要退出 + 2. After writing dependency files (if needed), you should proactively call shell tools to install dependencies ONLY if: + - The project has runtime dependencies that are required for execution + - The project explicitly requires build tools or package managers + - Do NOT install dependencies for simple vanilla web projects that can run without them - 4. 你的所有工具目录、安装、编写基于一个`workspace_dir`,该目录在你的工具中是`root`,你的所有文件相对目录均基于`workspace_dir` + 3. Any other code (including project files, other code files, git files, etc.) is not within your scope of work. You need to exit after dependency writing and installation are complete (or after determining that no dependencies are needed). + + 4. Your entire tool directory, installation, and writing are based on a `workspace_dir`, which is `root` in your tools. All your file relative paths are based on `workspace_dir`. max_chat_round: 20 @@ -42,13 +51,12 @@ max_chat_round: 20 tools: shell: mcp: false - timeout: 120 + timeout: 180 file_system: mcp: false include: - read_file - - read_abbreviation_file - write_file tool_call_timeout: 120 diff --git a/projects/code_genesis/orchestrator.yaml b/projects/code_genesis/orchestrator.yaml new file mode 100644 index 000000000..5a7403857 --- /dev/null +++ b/projects/code_genesis/orchestrator.yaml @@ -0,0 +1,98 @@ +llm: + service: openai + model: qwen3-coder-plus + openai_api_key: + openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 + + +generation_config: + temperature: 0.3 + top_k: 50 + max_tokens: 64000 + extra_body: + enable_thinking: false + dashscope_extend_params: + provider: d + + +prompt: + system: | + You are a Master Orchestrator, a senior architect responsible for comprehensive project planning and orchestration. Your role is to analyze user requirements and generate all necessary planning documents for software development. + + Your tasks: + 1. Generate topic.txt - Expand user requirements and determine the topic of the project + 2. Generate user_story.txt - Break down requirements into complete user stories (max 1000 words) + 3. Generate framework.txt - Select and document technology stack/framework choices (max 2000 words) + 4. Generate protocol.txt - Design communication protocols, addresses, ports, and framework access methods (max 1000 words) + 5. Generate file_design.txt - Design complete file structure in JSON format + 6. Generate file_order.txt - Arrange files in logical writing order based on dependencies + + Output Files: + + 1. topic.txt: + - Expanded user requirements + - Topic of the project + + 2. user_story.txt: + - Complete user stories based on requirements + - Include necessary features not mentioned by user + - Add runtime environment background (distributed systems, OS, deployment, etc.) + - For frontend projects: include HTML entry, JS/TS entry, build configs, CSS configs + + 3. framework.txt: + - Technology stack selection and rationale + - Framework usage guidelines and syntax rules + - Export/import conventions + - Dependency management approach + + 4. protocol.txt: + - Communication protocols and addresses + - Port configurations + - Framework access methods + - Project-level information only (not module-level) + + 5. file_design.txt (JSON format): + ```json + [ + { + "module": "module_name", + "files": [ + { + "name": "path/to/file.ext", + "description": "File description" + } + ] + } + ] + ``` + - Include all necessary files + + 6. file_order.txt (JSON format): + ```json + [ + { + "index": 0, + "files": ["package.json", "config.js"] + }, + { + "index": 1, + "files": ["utils/helper.js"] + } + ] + ``` + - Lower index = written first + - Files with dependencies should have higher index + - Files in same index can be written in parallel + - Entry files and README should have highest index + + Generate all six files: topic.txt, user_story.txt, framework.txt, protocol.txt, file_design.txt, and file_order.txt. + +max_chat_round: 10 + +tools: + file_system: + mcp: false + include: + - write_file + +tool_call_timeout: 30 diff --git a/projects/code_genesis/refine.yaml b/projects/code_genesis/refine.yaml index 944acbf33..6f6ded574 100644 --- a/projects/code_genesis/refine.yaml +++ b/projects/code_genesis/refine.yaml @@ -1,6 +1,6 @@ llm: service: openai - model: claude-sonnet-4-5-20250929 + model: qwen3-coder-plus openai_api_key: openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 @@ -16,51 +16,61 @@ generation_config: prompt: system: | - 你是一个优秀的软件测试工程师。你的职责是运行编写好的项目并修复运行时问题,并修复其中的问题。该项目由多个LLM编写,可能会由于模型幻觉或上下文缺失出现问题,常见问题有: - 1. 项目整体依赖或技术栈的LLM和具体代码文件的LLM出现技术栈差异 - 2. 编写不同代码文件的LLM引用方法名、输入输出结构异常 - 3. http、rpc协议异常 - 4. 对三方包使用错误 - - 你的工作流程: - 1. 上下文知识会直接放在user中,你无需再读取。这些知识包括: - * topic.txt:原始需求 - * protocol.txt:通讯协议 - * framework.txt:技术选型 - * tasks.txt: 项目生成文件列表 - - 2. 根据项目的技术栈,分析项目编译和启动方法,根据方法进行编译和运行 - - * 对后端 - a. 如果涉及到中间件,尝试启动它们 - b. 编译及运行后端,发现其中的错误 - c. 编写一定的测试代码,调用重要接口保证其可用性 - d. 如有可能,查看对应的存储媒介(例如数据库、json文件、缓存等),查看增删改查是否成功 - - * 对前端 - a. 运行npm run dev/build/dev等命令,根据结果进行修复 - - 进行最小的修复,防止健康代码被修改出错 - - 例子:服务提供端和使用端可能对不上,此时你需要分析,修改服务端和使用端哪个造成的二次问题可能性更低,并修改对应的部分 - b. 对重要页面进行`curl`等命令的调用,查看页面返回信息是否正确 - - * 算法和其他 - a. 编写测试用例,测试代码正确性 - - 3. 善于使用你的工具 - * 优先使用`read_abbreviation_file`读取缩略文件,如果信息不足,使用`read_file`读取原始文件,如果使用read_file,指定`start_line`和`end_line`参数部分读取减少token使用 - * `read_abbreviation_file`的缩略文件来自于前置的编码阶段。如果你发现所略微文件内容和实际对不上,使用使用`read_file`读取原始文件 - * 当你修改文件时,优先使用`replace_file_contents`来替换部分文件内容,如果必须重写文件,使用`write_file`进行存储 - * 如果http接口存疑,使用`workflow/api_search`搜索api接口实现 - * 通过`search_file_content`来在项目中搜索你感兴趣的关键字 - * shell命令不允许使用系统文件夹(/dev/null除外) - - 4. 一切都没问题后,你可以退出 - * 忽略变量未使用等warning,它们不影响实际运行 - * 你可以读取、更新代码,或安装npm、pip等依赖,或者使用curl发送请求,但不要执行修改系统的命令 - - 你的优化目标: - 1. 【优先】使项目的可用性达到最高 - 2. 【其次】使用尽量少的token数量 + You are an excellent software test engineer. Your job is to run the generated project and fix runtime issues (and any problems you find). This project was written by multiple LLMs, so it may contain issues caused by hallucinations or missing context. Common problems include: + 1. Tech-stack or dependency mismatches between the “overall” LLM and the LLM that wrote specific code files + 2. Inconsistent function/method references or incorrect input/output structures across different files + 3. HTTP/RPC protocol issues + 4. Incorrect usage of third-party packages + + Your workflow: + 1. Context will be provided directly in the user message; you do not need to read it again. This context includes: + * topic.txt: original requirements + * protocol.txt: communication protocol + * framework.txt: technology stack + * tasks.txt: generated file list + + 2. Based on the project's tech stack, determine how to build and start the project, then build and run it accordingly. + + * Backend + a. If middleware is involved, try starting it + b. Build and run the backend and fix any errors + c. Write minimal test code to call key APIs and ensure they work + d. If possible, inspect the underlying storage (e.g., database, JSON files, cache) and verify CRUD operations + + * Frontend + a. Run commands like `npm run dev` / `npm run build` and fix issues based on the results + - Apply minimal fixes to avoid breaking healthy code + - Example: the provider and consumer may be out of sync; analyze which side is less likely to cause secondary issues, and adjust that side + b. Use `curl` (or similar) to request important pages and verify the responses are correct + + * Algorithms and others + a. Write test cases to verify correctness + + 3. Use your tools effectively + * Use `read_file` to read the original file. When using `read_file`, specify `start_line` and `end_line` to reduce token usage + * When modifying files, prefer `edit_file` for partial edits; if you must rewrite an entire file, use `write_file` + * If an HTTP API is unclear, use `workflow/api_search` to locate the implementation + * Use `search_file_content` to search the project for keywords you care about + * Shell commands must not use system directories (except `/dev/null`) + + 4. When run successfully, you can use EdgeOne Pages MCP tools to deploy the project. Available tools from `edgeone-pages-mcp` include: + * Tools for creating projects, deploying, managing domains, etc. + * Check available tools by looking for tools prefixed with `edgeone-pages-mcp` + + Your workflow: + * You need to create a workspace_dir in the current working directory, and move necessary materials to the workspace_dir in the current working directory, filtering out unnecessary files like node_modules, dist, build, etc. + * Compress the workspace_dir into a zip file and name it workspace_dir.zip + * Once the project is working, use the EdgeOne Pages MCP tools to deploy it + * Follow the deployment documentation provided in the user message + * Upload the workspace_dir.zip to EdgeOne Pages and deploy it + + 5. If everything is OK, you may exit + * Ignore warnings like unused variables; they don't affect runtime behavior + * You may read/update code, install dependencies via npm/pip, or use curl to send requests, but do not run commands that modify the system + + Optimization goals: + 1. [Highest priority] Maximize project usability + 2. [Secondary] Use as few tokens as possible tools: shell: @@ -70,12 +80,22 @@ tools: include: - read_file - write_file - - read_abbreviation_file - - replace_file_contents - - delete_file_or_dir - list_files + - edit_file + edit_file_config: + diff_model: morph-v3-fast + api_key: + base_url: https://api.morphllm.com/v1 plugins: - workflow/api_search + edgeone-pages-mcp: + mcp: true + command: "npx" + args: ["edgeone-pages-mcp"] + env: + EDGEONE_PAGES_API_TOKEN: "" + timeout: 600 + sse_read_timeout: 600 max_chat_round: 1000 diff --git a/projects/code_genesis/simple_workflow.yaml b/projects/code_genesis/simple_workflow.yaml new file mode 100644 index 000000000..72689fe2b --- /dev/null +++ b/projects/code_genesis/simple_workflow.yaml @@ -0,0 +1,29 @@ +orchestrator: + agent_config: orchestrator.yaml + agent: + kwargs: + code_file: workflow/orchestrator.py + next: + - install + +install: + agent_config: install.yaml + agent: + kwargs: + code_file: workflow/install.py + next: + - coding + +coding: + agent_config: coding.yaml + agent: + kwargs: + code_file: workflow/coding.py + next: + - refine + +refine: + agent: + kwargs: + code_file: workflow/refine.py + agent_config: refine.yaml diff --git a/projects/code_genesis/user_story.yaml b/projects/code_genesis/user_story.yaml index 5d163b0ad..e58b20b72 100644 --- a/projects/code_genesis/user_story.yaml +++ b/projects/code_genesis/user_story.yaml @@ -1,44 +1,43 @@ llm: service: openai - model: claude-sonnet-4-5-20250929 + model: qwen3-coder-plus openai_api_key: openai_base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 generation_config: - temperature: 0.3 + temperature: 0.7 top_k: 50 max_tokens: 64000 extra_body: + enable_thinking: false dashscope_extend_params: - provider: b + provider: d prompt: system: | - 你是一个根据用户原始需求进行细节补充的产品经理。你的职责是根据用户提出的原始需求,分析其产品细节,并给出你的用户故事。你需要遵循下面的要求: + You are a product manager who enriches and refines requirements based on the user's original request. Your responsibility is to analyze the product details implied by the original request and produce user stories. You must follow these requirements: + + 1. Pay attention to the functions that need to be completed in the original requirements, break them down, and provide your complete user stories. + 2. Pay attention to the details explicitly mentioned by the user in the original requirements and supplement them fully. + 3. Complete features that the user did not mention but are necessary for a complete project. + 4. Pay special attention to adding background related to the application's runtime environment. For example: distributed systems, operating systems, deployment methods, microservices, etc. + 5. Store your user stories into user_story.txt, expressed precisely and concisely, no more than 1000 words. + 6. Use the same language for your user stories as the user's question. + 7. Do not write any code, and do not consider technical implementation issues. Technical design and coding will be handled later. - 1. 注意原始需求需要完成的功能,将它们细化,并给出你的完整用户故事 - 2. 注意原始需求中用户明确提到的细节,将它们补充完整 - 3. 完善用户未提及但作为一个完整的项目缺失的功能 - 4. 特别注意补充程序运行环境相关的背景。例如分布式、操作系统、部署方式、微服务等 - 5. 将你的用户故事存储进user_story.txt,精确、简洁表述,不超过1000个字 - 6. 不要编写任何代码,也不需要考虑技术实现问题,技术设计和代码工作会后续由专门的LLM进行 - 7. 你的用户故事语言和用户提问语言一致 max_chat_round: 20 tools: file_system: mcp: false - allow_read_all_files: true include: - - read_file - - read_abbreviation_file - write_file - - list_files memory: diversity: + num_split: 2 tool_call_timeout: 30 diff --git a/projects/code_genesis/workflow.yaml b/projects/code_genesis/workflow.yaml index 648c78d23..33fd11270 100644 --- a/projects/code_genesis/workflow.yaml +++ b/projects/code_genesis/workflow.yaml @@ -30,6 +30,14 @@ file_order: next: - install +orchestrator: + agent: + kwargs: + code_file: workflow/orchestrator.py + agent_config: orchestrator.yaml + next: + - install + install: agent_config: install.yaml agent: diff --git a/projects/code_genesis/workflow/architect.py b/projects/code_genesis/workflow/architect.py index 977ab631e..b340ab78a 100644 --- a/projects/code_genesis/workflow/architect.py +++ b/projects/code_genesis/workflow/architect.py @@ -8,7 +8,13 @@ class ArchitectureAgent(LLMAgent): async def run(self, messages, **kwargs): - query = '请读取对应文件并给出你的设计:' + with open(os.path.join(self.output_dir, 'topic.txt'), 'r') as f: + topic = f.read() + + with open(os.path.join(self.output_dir, 'user_story.txt'), 'r') as f: + user_story = f.read() + + query = f'Topic: {topic}\nUser Story: {user_story}\nPlease give your design.' messages = [ Message(role='system', content=self.config.prompt.system), diff --git a/projects/code_genesis/workflow/coding.py b/projects/code_genesis/workflow/coding.py index 0084f1093..364724875 100644 --- a/projects/code_genesis/workflow/coding.py +++ b/projects/code_genesis/workflow/coding.py @@ -69,7 +69,7 @@ def __init__(self, self.lsp_check = getattr(config, 'lsp_check', True) self.index_dir = os.path.join(self.output_dir, index_dir) self.lock_dir = os.path.join(self.output_dir, DEFAULT_LOCK_DIR) - self.code_condenser = CodeCondenser(config) + # self.code_condenser = CodeCondenser(config) self.code_files = [] self.shared_lsp_context = kwargs.get('shared_lsp_context', {}) self.unchecked_files = {} @@ -78,8 +78,8 @@ def __init__(self, self.find_all_files() self.error_counter = 0 - async def condense_memory(self, messages): - return messages + # async def condense_memory(self, messages): + # return messages async def add_memory(self, messages, **kwargs): return @@ -446,11 +446,11 @@ async def after_tool_call(self, messages: List[Message]): f'We check the code with LSP server and regex matching, here are the issues found:\n' f'{all_issues}\n' f'You can read related file to find the root cause if needed\n' - f'Then fix the file with `replace_file_contents`\n' + f'Then fix the file with `edit_file`\n' f'Some tips:\n' f'1. Check any code file not in your dependencies and not in the `file_design.txt`\n' f'2. Consider the relative path mistakes to your current writing file location\n' - f'3. Do not rewrite the code with after fixing with `replace_file_contents`\n' + f'3. Do not rewrite the code with after fixing with `edit_file`\n' ) messages.append(Message(role='user', content=all_issues)) messages[0].content = self.config.prompt.system @@ -485,7 +485,7 @@ async def after_tool_call(self, messages: List[Message]): )) # Condense code block and prepare index files - await self.code_condenser.run(messages) + # await self.code_condenser.run(messages) @dataclasses.dataclass @@ -574,24 +574,26 @@ async def _cleanup_lsp_servers(self): except Exception: # noqa pass - async def write_code(self, topic, user_story, framework, protocol, name, - description, index, last_batch, siblings, next_batch): + async def write_code(self, topic, user_story, framework, protocol, + file_order, name, description, index, last_batch, + siblings, next_batch): logger.info(f'Writing {name}') _config = deepcopy(self.config) messages = [ Message(role='system', content=self.config.prompt.system), Message( role='user', - content=f'原始需求(topic.txt): {topic}\n' - f'LLM规划的用户故事(user_story.txt): {user_story}\n' - f'技术栈(framework.txt): {framework}\n' - f'通讯协议(protocol.txt): {protocol}\n' - f'你需要编写的文件: {name}\n' - f'文件编写index: {index}\n' - f'文件描述: {description}\n' - f'上一批编写的代码:{last_batch}\n' - f'其他workers在并行编写:{siblings}\n' - f'下一批编写的代码:{next_batch}\n'), + content=f'Original requirements (topic.txt): {topic}\n' + f'User story planned by LLM (user_story.txt): {user_story}\n' + f'Tech stack (framework.txt): {framework}\n' + f'Communication protocol (protocol.txt): {protocol}\n' + f'File Order: {file_order}\n' + f'File to implement: {name}\n' + f'Batch index: {index}\n' + f'File description: {description}\n' + f'Previous batch output:\n{last_batch}\n' + f'Other workers writing in parallel:\n{siblings}\n' + f'Next batch planned:\n{next_batch}\n'), ] _config = deepcopy(self.config) @@ -615,6 +617,8 @@ async def execute_code(self, inputs, **kwargs): framework = f.read() with open(os.path.join(self.output_dir, 'protocol.txt')) as f: protocol = f.read() + with open(os.path.join(self.output_dir, 'file_order.txt')) as f: + file_order = f.read() file_orders = self.construct_file_orders() file_relation = OrderedDict() @@ -646,6 +650,7 @@ async def execute_code(self, inputs, **kwargs): user_story, framework, protocol, + file_order, name, description, index=idx, @@ -655,8 +660,6 @@ async def execute_code(self, inputs, **kwargs): for name, description in files.items() ] - # for task in tasks: - # await task await asyncio.gather(*tasks, return_exceptions=True) self.refresh_file_status(file_relation) @@ -718,13 +721,13 @@ def refresh_file_status(self, file_relation): file_relation[file_name].done = os.path.exists(file_path) def construct_file_information(self, file_relation, add_output_dir=False): - file_info = '以下文件按架构设计编写顺序排序:\n' + file_info = 'Files in architectural build order:\n' for file, relation in file_relation.items(): if add_output_dir: file = os.path.join(self.output_dir, file) if relation.done: - file_info += f'{file}: ✅已构建\n' + file_info += f'{file}: ✅Built\n' else: - file_info += f'{file}: ❌未构建\n' + file_info += f'{file}: ❌Not built\n' with open(os.path.join(self.output_dir, 'tasks.txt'), 'w') as f: f.write(file_info) diff --git a/projects/code_genesis/workflow/file_design.py b/projects/code_genesis/workflow/file_design.py index 784095d33..b33f977a5 100644 --- a/projects/code_genesis/workflow/file_design.py +++ b/projects/code_genesis/workflow/file_design.py @@ -9,7 +9,16 @@ class FileDesignAgent(LLMAgent): async def run(self, messages, **kwargs): - query = '请读取对应文件并给出你的设计:' + with open(os.path.join(self.output_dir, 'topic.txt'), 'r') as f: + topic = f.read() + + with open(os.path.join(self.output_dir, 'framework.txt'), 'r') as f: + framework = f.read() + + with open(os.path.join(self.output_dir, 'modules.txt'), 'r') as f: + modules = f.read() + + query = f'Topic: {topic}\nFramework: {framework}\nModules: {modules}\nPlease give your design.' messages = [ Message(role='system', content=self.config.prompt.system), @@ -17,16 +26,60 @@ async def run(self, messages, **kwargs): ] return await super().run(messages, **kwargs) + async def after_tool_call(self, messages: List[Message]): + await super().after_tool_call(messages) + + if self.runtime.should_stop: + query = None + + if os.path.isfile( + os.path.join(self.output_dir, 'file_design.txt')): + with open( + os.path.join(self.output_dir, 'file_design.txt'), + 'r') as f: + file_design = json.load(f) + + with open(os.path.join(self.output_dir, 'modules.txt'), + 'r') as f: + modules = f.readlines() + + files1 = set() + files2 = set() + for file in file_design: + name = file['module'] + files1.add(name) + + for module in modules: + files2.add(module.strip()) + + if len(files1) < len(files2): + query = ( + f'The file design you provided misses some modules: {files2 - files1}, ' + f'please provide the complete file order including these files.' + ) + elif len(files1) > len(files2): + query = ( + f'The file design you provided has some extra modules: {files1 - files2}, ' + f'please provide the correct file order without these files.' + ) + else: + query = ('The file design you provided is missing, ' + 'please provide the complete file design.') + + if query: + messages.append(Message(role='user', content=query)) + self.runtime.should_stop = False + async def on_task_end(self, messages: List[Message]): assert os.path.isfile(os.path.join(self.output_dir, 'file_design.txt')) with open(os.path.join(self.output_dir, 'file_design.txt'), 'r') as f: - contents = json.load(f) + file_design = json.load(f) with open(os.path.join(self.output_dir, 'modules.txt'), 'r') as f: modules = f.readlines() - assert len(modules) == len(contents) + assert len(modules) == len(file_design) - _modules = [content['module'] for content in contents] + _modules = [content['module'] for content in file_design] modules = [module.strip() for module in _modules if module.strip()] assert not (set(modules) - set(_modules)) diff --git a/projects/code_genesis/workflow/file_order.py b/projects/code_genesis/workflow/file_order.py index 340abeb13..592a68025 100644 --- a/projects/code_genesis/workflow/file_order.py +++ b/projects/code_genesis/workflow/file_order.py @@ -9,7 +9,16 @@ class FileOrderAgent(LLMAgent): async def run(self, messages, **kwargs): - query = '请读取file_design.txt并输出file_order.txt:' + with open(os.path.join(self.output_dir, 'topic.txt'), 'r') as f: + topic = f.read() + + with open(os.path.join(self.output_dir, 'framework.txt'), 'r') as f: + framework = f.read() + + with open(os.path.join(self.output_dir, 'file_design.txt'), 'r') as f: + file_design = f.read() + + query = f'Topic: {topic}\nFramework: {framework}\nFile Design: {file_design}\nPlease give your file order.' messages = [ Message(role='system', content=self.config.prompt.system), @@ -17,6 +26,50 @@ async def run(self, messages, **kwargs): ] return await super().run(messages, **kwargs) + async def after_tool_call(self, messages: List[Message]): + await super().after_tool_call(messages) + + if self.runtime.should_stop: + query = None + + if os.path.isfile(os.path.join(self.output_dir, 'file_order.txt')): + with open( + os.path.join(self.output_dir, 'file_order.txt'), + 'r') as f: + file_order = json.load(f) + + with open( + os.path.join(self.output_dir, 'file_design.txt'), + 'r') as f: + file_design = json.load(f) + + files1 = set() + files2 = set() + for file in file_order: + files1.update(file['files']) + + for file in file_design: + names = [f['name'] for f in file['files']] + files2.update(names) + + if len(files1) < len(files2): + query = ( + f'The file order you provided misses some files: {files2 - files1}, ' + f'please provide the complete file order including these files.' + ) + elif len(files1) > len(files2): + query = ( + f'The file order you provided has some extra files: {files1 - files2}, ' + f'please provide the correct file order without these files.' + ) + else: + query = ('The file order you provided is missing, ' + 'please provide the complete file order.') + + if query: + messages.append(Message(role='user', content=query)) + self.runtime.should_stop = False + async def on_task_end(self, messages: List[Message]): assert os.path.isfile(os.path.join(self.output_dir, 'file_order.txt')) with open(os.path.join(self.output_dir, 'file_order.txt'), 'r') as f: diff --git a/projects/code_genesis/workflow/install.py b/projects/code_genesis/workflow/install.py index c5e9d20fc..5dfb16cb8 100644 --- a/projects/code_genesis/workflow/install.py +++ b/projects/code_genesis/workflow/install.py @@ -1,3 +1,5 @@ +import os + from ms_agent import LLMAgent from ms_agent.llm import Message @@ -5,7 +7,19 @@ class InstallAgent(LLMAgent): async def run(self, messages, **kwargs): - query = f'你的`workflow_dir`是{self.output_dir}, 请编写依赖文件并安装依赖:' + with open(os.path.join(self.output_dir, 'topic.txt'), 'r') as f: + topic = f.read() + + with open(os.path.join(self.output_dir, 'framework.txt'), 'r') as f: + framework = f.read() + + with open(os.path.join(self.output_dir, 'file_design.txt'), 'r') as f: + file_design = f.read() + + query = ( + f'Topic: {topic}\nFramework: {framework}\nFile Design: {file_design}\n' + f'Your `workflow_dir` is {self.output_dir}, ' + 'Please write dependency files and install dependencies.') messages = [ Message(role='system', content=self.config.prompt.system), diff --git a/projects/code_genesis/workflow/orchestrator.py b/projects/code_genesis/workflow/orchestrator.py new file mode 100644 index 000000000..b10bb80a2 --- /dev/null +++ b/projects/code_genesis/workflow/orchestrator.py @@ -0,0 +1,30 @@ +import os +from typing import List + +from ms_agent import LLMAgent +from ms_agent.llm import Message + + +class OrchestratorAgent(LLMAgent): + """Master Orchestrator - Coordinates file writing order based on project requirements""" + + async def run(self, user_input, **kwargs): + query = ( + f'Project Requirements: {user_input}\n\n' + f'Please generate all planning documents: ' + f'topic.txt, user_story.txt, framework.txt, protocol.txt, file_design.txt, and file_order.txt' + ) + + messages = [ + Message(role='system', content=self.config.prompt.system), + Message(role='user', content=query), + ] + return await super().run(messages, **kwargs) + + async def on_task_end(self, messages: List[Message]): + assert os.path.isfile(os.path.join(self.output_dir, 'topic.txt')) + assert os.path.isfile(os.path.join(self.output_dir, 'user_story.txt')) + assert os.path.isfile(os.path.join(self.output_dir, 'framework.txt')) + assert os.path.isfile(os.path.join(self.output_dir, 'protocol.txt')) + assert os.path.isfile(os.path.join(self.output_dir, 'file_design.txt')) + assert os.path.isfile(os.path.join(self.output_dir, 'file_order.txt')) diff --git a/projects/code_genesis/workflow/refine.py b/projects/code_genesis/workflow/refine.py index 75424cd62..d99272ab3 100644 --- a/projects/code_genesis/workflow/refine.py +++ b/projects/code_genesis/workflow/refine.py @@ -43,14 +43,14 @@ async def run(self, messages, **kwargs): Message(role='system', content=self.config.prompt.system), Message( role='user', - content=f'原始需求(topic.txt): {topic}\n' - f'技术栈(framework.txt): {framework}\n' - f'通讯协议(protocol.txt): {protocol}\n' - f'文件列表:{file_info}\n' - f'你的shell工具的workspace_dir是{self.output_dir},' - f'你使用的工具均以该目录作为当前目录.\n' - f'python环境是: {sys.executable}\n' - f'请针对项目进行refine:'), + content=f'Original requirements (topic.txt): {topic}\n' + f'Tech stack (framework.txt): {framework}\n' + f'Communication protocol (protocol.txt): {protocol}\n' + f'File list:\n{file_info}\n' + f'Your shell tool workspace_dir is {self.output_dir}; ' + f'all tools should use this directory as the current working directory.\n' + f'Python executable: {sys.executable}\n' + f'Please refine the project and deploy it to EdgeOne Pages:'), ] return await super().run(messages, **kwargs) diff --git a/webui/backend/agent_runner.py b/webui/backend/agent_runner.py index afe50ea16..292de897a 100644 --- a/webui/backend/agent_runner.py +++ b/webui/backend/agent_runner.py @@ -12,6 +12,8 @@ from datetime import datetime from typing import Any, Callable, Dict, Optional +import yaml + class AgentRunner: """Runs ms-agent as a subprocess with output streaming""" @@ -24,7 +26,8 @@ def __init__(self, on_log: Callable[[Dict[str, Any]], None] = None, on_progress: Callable[[Dict[str, Any]], None] = None, on_complete: Callable[[Dict[str, Any]], None] = None, - on_error: Callable[[Dict[str, Any]], None] = None): + on_error: Callable[[Dict[str, Any]], None] = None, + workflow_type: str = 'standard'): self.session_id = session_id self.project = project self.config_manager = config_manager @@ -33,6 +36,7 @@ def __init__(self, self.on_progress = on_progress self.on_complete = on_complete self.on_error = on_error + self._workflow_type = workflow_type self.process: Optional[asyncio.subprocess.Process] = None self.is_running = False @@ -132,6 +136,16 @@ def _build_command(self, query: str) -> list: project_path = self.project['path'] config_file = self.project.get('config_file', '') + # Get workflow_type from session if available + # This allows switching between standard and simple workflow for code_genesis + workflow_type = getattr(self, '_workflow_type', 'standard') + if workflow_type == 'simple' and project_type == 'workflow': + # For code_genesis with simple workflow, use simple_workflow.yaml + simple_config_file = os.path.join(project_path, + 'simple_workflow.yaml') + if os.path.exists(simple_config_file): + config_file = simple_config_file + # Get python executable python = sys.executable @@ -151,7 +165,7 @@ def _build_command(self, query: str) -> list: if os.path.exists(mcp_file): cmd.extend(['--mcp_server_file', mcp_file]) - # Add LLM config + # Add LLM config from user settings llm_config = self.config_manager.get_llm_config() if llm_config.get('api_key'): provider = llm_config.get('provider', 'modelscope') @@ -159,6 +173,74 @@ def _build_command(self, query: str) -> list: cmd.extend(['--modelscope_api_key', llm_config['api_key']]) elif provider == 'openai': cmd.extend(['--openai_api_key', llm_config['api_key']]) + # Set llm.service to openai to ensure the correct service is used + cmd.extend(['--llm.service', 'openai']) + # Pass base_url if set by user + if llm_config.get('base_url'): + cmd.extend( + ['--llm.openai_base_url', llm_config['base_url']]) + # Pass model if set by user + if llm_config.get('model'): + cmd.extend(['--llm.model', llm_config['model']]) + # Pass temperature if set by user (in generation_config) + if llm_config.get('temperature') is not None: + cmd.extend([ + '--generation_config.temperature', + str(llm_config['temperature']) + ]) + # Pass max_tokens if set by user (in generation_config) + if llm_config.get('max_tokens'): + cmd.extend([ + '--generation_config.max_tokens', + str(llm_config['max_tokens']) + ]) + + # Add edit_file_config from user settings + edit_file_config = self.config_manager.get_edit_file_config() + if edit_file_config.get('api_key'): + # If API key is provided, pass edit_file_config + cmd.extend([ + '--tools.file_system.edit_file_config.api_key', + edit_file_config['api_key'] + ]) + if edit_file_config.get('base_url'): + cmd.extend([ + '--tools.file_system.edit_file_config.base_url', + edit_file_config['base_url'] + ]) + if edit_file_config.get('diff_model'): + cmd.extend([ + '--tools.file_system.edit_file_config.diff_model', + edit_file_config['diff_model'] + ]) + else: + # If no API key, exclude edit_file from tools + # Read the current include list from config file and remove edit_file + try: + with open(config_file, 'r', encoding='utf-8') as f: + config_data = yaml.safe_load(f) + if config_data and 'tools' in config_data and 'file_system' in config_data[ + 'tools']: + include_list = config_data['tools']['file_system'].get( + 'include', []) + if isinstance(include_list, + list) and 'edit_file' in include_list: + # Remove edit_file from the list + filtered_include = [ + tool for tool in include_list + if tool != 'edit_file' + ] + # Pass the filtered list as comma-separated string + cmd.extend([ + '--tools.file_system.include', + ','.join(filtered_include) + ]) + except Exception as e: + print( + f'[Runner] Warning: Could not read config file to exclude edit_file: {e}' + ) + # Fallback: explicitly exclude edit_file + cmd.extend(['--tools.file_system.exclude', 'edit_file']) elif project_type == 'script': # Run the script directly @@ -263,6 +345,88 @@ def _detect_log_level(self, line: str) -> str: async def _detect_patterns(self, line: str): """Detect special patterns in output""" + # Detect OpenAI API errors and other API errors + # Check for OpenAI error patterns + if 'openai.' in line.lower() and ('error' in line.lower() + or 'Error' in line): + error_message = line.strip() + # Try to extract error details from the line + # Pattern: openai.NotFoundError: Error code: 404 - {'error': {'message': '...', ...}} + json_match = re.search(r'\{.*?\}', error_message, re.DOTALL) + if json_match: + try: + import json + error_data = json.loads(json_match.group(0)) + if 'error' in error_data and 'message' in error_data[ + 'error']: + error_msg = error_data['error']['message'] + error_type = error_data['error'].get( + 'type', 'API Error') + error_message = f'**{error_type}**: {error_msg}' + except Exception: + pass + + print(f'[Runner] Detected API error: {error_message}') + if self.on_error: + self.on_error({'message': error_message, 'type': 'api_error'}) + # Also send as output message so it appears in the conversation + if self.on_output: + self.on_output({ + 'type': 'error', + 'content': error_message, + 'role': 'system', + 'metadata': { + 'error_type': 'api_error' + } + }) + return + + # Detect other error patterns + error_patterns = [ + r'Error code:\s*(\d+)\s*-\s*({.*?})', + ] + + for pattern in error_patterns: + error_match = re.search(pattern, line, re.IGNORECASE | re.DOTALL) + if error_match: + error_message = line.strip() + # Try to extract JSON error details if available + json_match = re.search(r'\{.*?\}', error_message, re.DOTALL) + if json_match: + try: + import json + error_data = json.loads(json_match.group(0)) + if 'error' in error_data and 'message' in error_data[ + 'error']: + error_msg = error_data['error']['message'] + error_type = error_data['error'].get( + 'type', 'API Error') + error_message = f'**{error_type}**: {error_msg}' + except Exception: + pass + + print(f'[Runner] Detected API error: {error_message}') + if self.on_error: + self.on_error({ + 'message': + error_message, + 'type': + 'api_error', + 'code': + error_match.group(1) if error_match.groups() else None + }) + # Also send as output message so it appears in the conversation + if self.on_output: + self.on_output({ + 'type': 'error', + 'content': error_message, + 'role': 'system', + 'metadata': { + 'error_type': 'api_error' + } + }) + return + # Detect workflow step beginning: "[tag] Agent tag task beginning." begin_match = re.search( r'\[([^\]]+)\]\s*Agent\s+\S+\s+task\s+beginning', line) diff --git a/webui/backend/api.py b/webui/backend/api.py index c33eda324..6576d203e 100644 --- a/webui/backend/api.py +++ b/webui/backend/api.py @@ -23,11 +23,14 @@ class ProjectInfo(BaseModel): type: str # 'workflow' or 'agent' path: str has_readme: bool + supports_workflow_switch: bool = False class SessionCreate(BaseModel): project_id: str query: Optional[str] = None + workflow_type: Optional[ + str] = 'standard' # 'standard' or 'simple' for code_genesis class SessionInfo(BaseModel): @@ -47,6 +50,12 @@ class LLMConfig(BaseModel): max_tokens: int = 4096 +class EditFileConfig(BaseModel): + api_key: Optional[str] = None + base_url: str = 'https://api.morphllm.com/v1' + diff_model: str = 'morph-v3-fast' + + class MCPServer(BaseModel): name: str type: str # 'stdio' or 'sse' @@ -88,6 +97,27 @@ async def get_project_readme(project_id: str): return {'content': readme} +@router.get('/projects/{project_id}/workflow') +async def get_project_workflow(project_id: str): + """Get the workflow configuration for a project""" + project = project_discovery.get_project(project_id) + if not project: + raise HTTPException(status_code=404, detail='Project not found') + + workflow_file = os.path.join(project['path'], 'workflow.yaml') + if not os.path.exists(workflow_file): + raise HTTPException(status_code=404, detail='Workflow file not found') + + try: + import yaml + with open(workflow_file, 'r', encoding='utf-8') as f: + workflow_data = yaml.safe_load(f) + return {'workflow': workflow_data} + except Exception as e: + raise HTTPException( + status_code=500, detail=f'Error reading workflow file: {str(e)}') + + # Session Endpoints @router.post('/sessions', response_model=SessionInfo) async def create_session(session_data: SessionCreate): @@ -96,8 +126,18 @@ async def create_session(session_data: SessionCreate): if not project: raise HTTPException(status_code=404, detail='Project not found') + # Validate workflow_type for projects that support switching + workflow_type = session_data.workflow_type or 'standard' + if project.get('supports_workflow_switch'): + if workflow_type not in ['standard', 'simple']: + raise HTTPException( + status_code=400, + detail="workflow_type must be 'standard' or 'simple'") + session = session_manager.create_session( - project_id=session_data.project_id, project_name=project['name']) + project_id=session_data.project_id, + project_name=project['name'], + workflow_type=workflow_type) return session @@ -174,6 +214,19 @@ async def update_mcp_config(servers: Dict[str, Any]): return {'status': 'updated'} +@router.get('/config/edit_file') +async def get_edit_file_config(): + """Get edit_file_config configuration""" + return config_manager.get_edit_file_config() + + +@router.put('/config/edit_file') +async def update_edit_file_config(config: EditFileConfig): + """Update edit_file_config configuration""" + config_manager.update_edit_file_config(config.model_dump()) + return {'status': 'updated'} + + @router.post('/config/mcp/servers') async def add_mcp_server(server: MCPServer): """Add a new MCP server""" diff --git a/webui/backend/config_manager.py b/webui/backend/config_manager.py index 0af0f2d4c..9ce03703b 100644 --- a/webui/backend/config_manager.py +++ b/webui/backend/config_manager.py @@ -22,15 +22,21 @@ class ConfigManager: 'temperature': 0.7, 'max_tokens': 4096 }, + 'edit_file_config': { + 'api_key': '', + 'base_url': 'https://api.morphllm.com/v1', + 'diff_model': 'morph-v3-fast' + }, 'mcp_servers': {}, 'theme': 'dark', 'output_dir': './output' } def __init__(self, config_dir: str): - self.config_dir = config_dir - self.config_file = os.path.join(config_dir, 'settings.json') - self.mcp_file = os.path.join(config_dir, 'mcp_servers.json') + # Expand user path to handle ~ notation + self.config_dir = os.path.expanduser(config_dir) + self.config_file = os.path.join(self.config_dir, 'settings.json') + self.mcp_file = os.path.join(self.config_dir, 'mcp_servers.json') self._lock = Lock() self._config: Optional[Dict[str, Any]] = None self._ensure_config_dir() @@ -118,6 +124,18 @@ def update_mcp_config(self, mcp_config: Dict[str, Any]): self._config['mcp_servers'] = mcp_config self._save_config() + def get_edit_file_config(self) -> Dict[str, Any]: + """Get edit_file_config configuration""" + config = self._load_config() + return config.get('edit_file_config', + self.DEFAULT_CONFIG['edit_file_config']) + + def update_edit_file_config(self, edit_file_config: Dict[str, Any]): + """Update edit_file_config configuration""" + self._load_config() + self._config['edit_file_config'] = edit_file_config + self._save_config() + def add_mcp_server(self, name: str, server_config: Dict[str, Any]): """Add a new MCP server""" self._load_config() diff --git a/webui/backend/project_discovery.py b/webui/backend/project_discovery.py index 6a74da127..b4895636b 100644 --- a/webui/backend/project_discovery.py +++ b/webui/backend/project_discovery.py @@ -43,6 +43,7 @@ def _analyze_project(self, name: str, """Analyze a project directory and extract its information""" # Check for workflow.yaml or agent.yaml workflow_file = os.path.join(path, 'workflow.yaml') + simple_workflow_file = os.path.join(path, 'simple_workflow.yaml') agent_file = os.path.join(path, 'agent.yaml') run_file = os.path.join(path, 'run.py') readme_file = os.path.join(path, 'README.md') @@ -61,6 +62,12 @@ def _analyze_project(self, name: str, # Skip directories without valid config return None + # Check if project supports workflow switching (e.g., code_genesis) + supports_workflow_switch = False + if project_type == 'workflow' and name == 'code_genesis' and os.path.exists( + simple_workflow_file): + supports_workflow_switch = True + # Generate display name from directory name display_name = self._format_display_name(name) @@ -76,7 +83,8 @@ def _analyze_project(self, name: str, 'type': project_type, 'path': path, 'has_readme': os.path.exists(readme_file), - 'config_file': config_file + 'config_file': config_file, + 'supports_workflow_switch': supports_workflow_switch } def _format_display_name(self, name: str) -> str: diff --git a/webui/backend/session_manager.py b/webui/backend/session_manager.py index d588fbaf3..eb758d96f 100644 --- a/webui/backend/session_manager.py +++ b/webui/backend/session_manager.py @@ -17,8 +17,10 @@ def __init__(self): self._messages: Dict[str, List[Dict[str, Any]]] = {} self._lock = Lock() - def create_session(self, project_id: str, - project_name: str) -> Dict[str, Any]: + def create_session(self, + project_id: str, + project_name: str, + workflow_type: str = 'standard') -> Dict[str, Any]: """Create a new session""" session_id = str(uuid.uuid4()) session = { @@ -29,7 +31,8 @@ def create_session(self, project_id: str, 'created_at': datetime.now().isoformat(), 'workflow_progress': None, 'file_progress': None, - 'current_step': None + 'current_step': None, + 'workflow_type': workflow_type # 'standard' or 'simple' } with self._lock: diff --git a/webui/backend/shared.py b/webui/backend/shared.py index 08dbd5775..1d3ce9754 100644 --- a/webui/backend/shared.py +++ b/webui/backend/shared.py @@ -13,7 +13,8 @@ BASE_DIR = os.path.dirname( os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) PROJECTS_DIR = os.path.join(BASE_DIR, 'projects') -CONFIG_DIR = os.path.join(BASE_DIR, 'webui', 'config') +# Use ~/.ms_agent/ for configuration storage (privacy-sensitive data) +CONFIG_DIR = os.path.expanduser('~/.ms_agent') # Shared instances project_discovery = ProjectDiscovery(PROJECTS_DIR) diff --git a/webui/backend/websocket_handler.py b/webui/backend/websocket_handler.py index 649969171..dc842f3dd 100644 --- a/webui/backend/websocket_handler.py +++ b/webui/backend/websocket_handler.py @@ -147,9 +147,11 @@ async def start_agent(session_id: str, data: Dict[str, Any], }) return - print( - f"[Agent] Project: {project['id']}, type: {project['type']}, config: {project['config_file']}" - ) + # Get workflow_type from session (default to 'standard') + workflow_type = session.get('workflow_type', 'standard') + + print(f"[Agent] Project: {project['id']}, type: {project['type']}, " + f"config: {project['config_file']}, workflow_type: {workflow_type}") query = data.get('query', '') print(f'[Agent] Query: {query[:100]}...' @@ -158,7 +160,7 @@ async def start_agent(session_id: str, data: Dict[str, Any], # Add user message to session (but don't broadcast - frontend already has it) session_manager.add_message(session_id, 'user', query, 'text') - # Create agent runner + # Create agent runner with workflow_type runner = AgentRunner( session_id=session_id, project=project, @@ -171,7 +173,8 @@ async def start_agent(session_id: str, data: Dict[str, Any], on_complete=lambda result: asyncio.create_task( on_agent_complete(session_id, result)), on_error=lambda err: asyncio.create_task( - on_agent_error(session_id, err))) + on_agent_error(session_id, err)), + workflow_type=workflow_type) agent_runners[session_id] = runner session_manager.update_session(session_id, {'status': 'running'}) diff --git a/webui/config/mcp_servers.json b/webui/config/mcp_servers.json deleted file mode 100644 index da39e4ffa..000000000 --- a/webui/config/mcp_servers.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "mcpServers": {} -} diff --git a/webui/config/settings.json b/webui/config/settings.json deleted file mode 100644 index 26d2bd591..000000000 --- a/webui/config/settings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "llm": { - "provider": "openai", - "model": "qwen3-coder-plus", - "api_key": "", - "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", - "temperature": 0.7, - "max_tokens": 32768 - }, - "theme": "dark", - "output_dir": "./output" -} diff --git a/webui/frontend/src/components/ConversationView.tsx b/webui/frontend/src/components/ConversationView.tsx index 99c4dcb94..cade215f6 100644 --- a/webui/frontend/src/components/ConversationView.tsx +++ b/webui/frontend/src/components/ConversationView.tsx @@ -10,7 +10,6 @@ import { alpha, Chip, Divider, - Avatar, Tooltip, Dialog, DialogTitle, @@ -20,24 +19,22 @@ import { import { Send as SendIcon, Stop as StopIcon, - Person as PersonIcon, - AutoAwesome as BotIcon, PlayArrow as RunningIcon, InsertDriveFile as FileIcon, Code as CodeIcon, Description as DocIcon, Image as ImageIcon, - CheckCircle as CompleteIcon, - HourglassTop as StartIcon, Close as CloseIcon, ContentCopy as CopyIcon, Folder as FolderIcon, FolderOpen as FolderOpenIcon, ChevronRight as ChevronRightIcon, ExpandMore as ExpandMoreIcon, + CheckCircle as CheckCircleIcon, + AccountTree as WorkflowIcon, } from '@mui/icons-material'; import { motion, AnimatePresence } from 'framer-motion'; -import { useSession, Message, Session } from '../context/SessionContext'; +import { useSession, Message } from '../context/SessionContext'; import WorkflowProgress from './WorkflowProgress'; import FileProgress from './FileProgress'; import LogViewer from './LogViewer'; @@ -60,16 +57,11 @@ const ConversationView: React.FC = ({ showLogs }) => { logs, } = useSession(); - const completedSteps = React.useMemo(() => { - const set = new Set(); - for (const m of messages) { - if (m.type === 'step_complete' && m.content) set.add(m.content); - } - return set; - }, [messages]); - const [input, setInput] = useState(''); const [outputFilesOpen, setOutputFilesOpen] = useState(false); + const [workflowOpen, setWorkflowOpen] = useState(false); + const [workflowData, setWorkflowData] = useState | null>(null); + const [workflowLoading, setWorkflowLoading] = useState(false); // eslint-disable-next-line @typescript-eslint/no-explicit-any const [outputTree, setOutputTree] = useState({folders: {}, files: []}); const [expandedFolders, setExpandedFolders] = useState>(new Set()); @@ -155,6 +147,28 @@ const ConversationView: React.FC = ({ showLogs }) => { } }; + const loadWorkflow = async () => { + if (!currentSession?.project_id) return; + + setWorkflowLoading(true); + try { + const response = await fetch(`/api/projects/${currentSession.project_id}/workflow`); + if (response.ok) { + const data = await response.json(); + setWorkflowData(data.workflow || {}); + } + } catch (err) { + console.error('Failed to load workflow:', err); + } finally { + setWorkflowLoading(false); + } + }; + + const handleOpenWorkflow = () => { + loadWorkflow(); + setWorkflowOpen(true); + }; + return ( = ({ showLogs }) => { borderRadius: '8px', }} /> + {currentSession?.project_id && ( + + } + label="Workflow" + size="small" + onClick={handleOpenWorkflow} + sx={{ + backgroundColor: alpha(theme.palette.secondary.main, 0.1), + color: theme.palette.secondary.main, + cursor: 'pointer', + '&:hover': { + backgroundColor: alpha(theme.palette.secondary.main, 0.2), + }, + }} + /> + + )} : undefined} label={currentSession?.status} @@ -304,6 +336,47 @@ const ConversationView: React.FC = ({ showLogs }) => { + {/* Workflow Dialog */} + setWorkflowOpen(false)} + maxWidth={false} + fullWidth + PaperProps={{ + sx: { + backgroundColor: theme.palette.background.paper, + maxWidth: '95vw', + width: '95vw', + height: '85vh', + display: 'flex', + flexDirection: 'column', + } + }} + > + + + + Workflow + + setWorkflowOpen(false)}> + + + + + {workflowLoading ? ( + + + + ) : workflowData ? ( + + ) : ( + + No workflow data available + + )} + + + {/* Main Content Area */} {/* Messages Area */} @@ -320,11 +393,11 @@ const ConversationView: React.FC = ({ showLogs }) => { sx={{ flex: 1, overflowY: 'auto', - px: 3, - py: 3, + px: 2, + py: 2, display: 'flex', flexDirection: 'column', - gap: 2.5, + gap: 1, '&::-webkit-scrollbar': { width: 6, }, @@ -342,8 +415,6 @@ const ConversationView: React.FC = ({ showLogs }) => { ))} @@ -362,51 +433,54 @@ const ConversationView: React.FC = ({ showLogs }) => { timestamp: new Date().toISOString(), }} isStreaming - sessionStatus={currentSession?.status} - completedSteps={completedSteps} /> )} - {/* Loading Indicator */} + {/* Loading Indicator - Shows current step in progress */} {isLoading && !isStreaming && messages.length > 0 && (() => { - // Find current running step + // Find current running step (step_start without corresponding step_complete) const runningSteps = messages.filter(m => m.type === 'step_start'); - const completedSteps = messages.filter(m => m.type === 'step_complete'); - const currentStep = runningSteps.length > completedSteps.length - ? runningSteps[runningSteps.length - 1]?.content?.replace(/_/g, ' ') - : null; + const completedStepsSet = new Set( + messages.filter(m => m.type === 'step_complete').map(m => m.content) + ); + + // Find the last step_start that hasn't been completed yet + const currentRunningStep = runningSteps + .slice() + .reverse() + .find(step => !completedStepsSet.has(step.content)); + + const currentStepName = currentRunningStep?.content?.replace(/_/g, ' ') || null; + + if (!currentStepName) { + return null; // No step in progress, don't show indicator + } return ( - - - @@ -436,14 +510,10 @@ const ConversationView: React.FC = ({ showLogs }) => { ))} - {currentStep ? ( - <> - - {currentStep} - - in progress... - - ) : 'Processing...'} + + {currentStepName} + + in progress... @@ -530,11 +600,9 @@ const ConversationView: React.FC = ({ showLogs }) => { interface MessageBubbleProps { message: Message; isStreaming?: boolean; - sessionStatus?: Session['status']; - completedSteps?: Set; } -const MessageBubble: React.FC = ({ message, isStreaming, sessionStatus, completedSteps }) => { +const MessageBubble: React.FC = ({ message, isStreaming }) => { const theme = useTheme(); const isUser = message.role === 'user'; const isError = message.type === 'error'; @@ -551,77 +619,79 @@ const MessageBubble: React.FC = ({ message, isStreaming, ses if (isSystem && message.content.startsWith('Starting step:')) return null; if (isSystem && message.content.startsWith('Completed step:')) return null; - // Step start/complete display - if (isStepStart || isStepComplete) { - // If a step has a completion record, hide the earlier start record to avoid duplicates. - if (isStepStart && completedSteps?.has(message.content)) { - return null; - } + // Hide step_start messages (they're shown in Loading Indicator instead) + if (isStepStart) { + return null; + } + // Convert step_complete to regular assistant message bubble with checkmark + if (isStepComplete) { const stepName = message.content.replace(/_/g, ' '); - const isComplete = isStepComplete || (isStepStart && !!completedSteps?.has(message.content)); - const isStopped = isStepStart && !isComplete && sessionStatus === 'stopped'; - const accentColor = isComplete - ? theme.palette.success.main - : isStopped - ? theme.palette.warning.main - : theme.palette.info.main; + const completedText = `${stepName.charAt(0).toUpperCase() + stepName.slice(1)} completed`; + // Render as regular assistant message with checkmark icon return ( - - {isComplete ? : } - - - - {stepName} - - - {isComplete ? 'Completed' : isStopped ? 'Stopped' : 'Running...'} - - + + + + + + ); @@ -647,60 +717,29 @@ const MessageBubble: React.FC = ({ message, isStreaming, ses - {/* Avatar */} - - - {isUser ? : } - - - - {/* Message Content */} + {/* Message Content - 简洁的椭圆形对话框 */} @@ -731,6 +770,366 @@ const MessageBubble: React.FC = ({ message, isStreaming, ses export default ConversationView; +// Workflow View Component +interface WorkflowViewProps { + workflow: Record; + currentStep?: string; +} + +const WorkflowView: React.FC = ({ workflow, currentStep }) => { + const theme = useTheme(); + const containerRef = useRef(null); + const nodeRefs = useRef>({}); + + // Node positions state + const [nodePositions, setNodePositions] = useState>({}); + const [nodeHeights, setNodeHeights] = useState>({}); + const [draggingNode, setDraggingNode] = useState(null); + const [dragOffset, setDragOffset] = useState<{ x: number; y: number }>({ x: 0, y: 0 }); + + // Build workflow graph structure + const buildWorkflowGraph = () => { + const nodes: Array<{ id: string; name: string; config: string }> = []; + const edges: Array<{ from: string; to: string }> = []; + const hasIncoming = new Set(); + + Object.entries(workflow).forEach(([key, value]: [string, any]) => { + nodes.push({ + id: key, + name: key.replace(/_/g, ' '), + config: value.agent_config || '', + }); + + if (value.next) { + const nextSteps = Array.isArray(value.next) ? value.next : [value.next]; + nextSteps.forEach((next: string) => { + hasIncoming.add(next); + edges.push({ from: key, to: next }); + }); + } + }); + + return { nodes, edges }; + }; + + const { nodes, edges } = buildWorkflowGraph(); + + // Initialize node positions in a horizontal flow layout + useEffect(() => { + if (Object.keys(nodePositions).length === 0 && nodes.length > 0) { + const positions: Record = {}; + const visited = new Set(); + + // Find root nodes + const hasIncoming = new Set(); + edges.forEach(e => hasIncoming.add(e.to)); + const rootNodes = nodes.filter(n => !hasIncoming.has(n.id)); + + // BFS to assign positions + const queue: Array<{ id: string; level: number }> = []; + rootNodes.forEach((node) => { + queue.push({ id: node.id, level: 0 }); + }); + + const levelNodes: Record = {}; + + while (queue.length > 0) { + const { id, level } = queue.shift()!; + if (visited.has(id)) continue; + visited.add(id); + + if (!levelNodes[level]) levelNodes[level] = []; + levelNodes[level].push(id); + + const outgoing = edges.filter(e => e.from === id); + outgoing.forEach(edge => { + if (!visited.has(edge.to)) { + const nextLevel = level + 1; + queue.push({ id: edge.to, level: nextLevel }); + } + }); + } + + // Calculate positions + const horizontalSpacing = 200; + const verticalSpacing = 100; + + Object.entries(levelNodes).forEach(([levelStr, nodeIds]) => { + const level = parseInt(levelStr); + const startX = 100 + level * horizontalSpacing; + const totalHeight = nodeIds.length * verticalSpacing; + const startY = 100 - totalHeight / 2 + verticalSpacing / 2; + + nodeIds.forEach((nodeId, idx) => { + positions[nodeId] = { + x: startX, + y: startY + idx * verticalSpacing, + }; + }); + }); + + setNodePositions(positions); + } + }, [nodes, edges, nodePositions]); + + // Handle drag start + const handleMouseDown = (e: React.MouseEvent, nodeId: string) => { + if (!containerRef.current) return; + + const rect = containerRef.current.getBoundingClientRect(); + const nodePos = nodePositions[nodeId]; + if (!nodePos) return; + + setDraggingNode(nodeId); + setDragOffset({ + x: e.clientX - rect.left - nodePos.x, + y: e.clientY - rect.top - nodePos.y, + }); + }; + + // Handle drag + useEffect(() => { + if (!draggingNode || !containerRef.current) return; + + const handleMouseMove = (e: MouseEvent) => { + const rect = containerRef.current!.getBoundingClientRect(); + const newX = e.clientX - rect.left - dragOffset.x; + const newY = e.clientY - rect.top - dragOffset.y; + + setNodePositions(prev => ({ + ...prev, + [draggingNode]: { x: newX, y: newY }, + })); + }; + + const handleMouseUp = () => { + setDraggingNode(null); + }; + + window.addEventListener('mousemove', handleMouseMove); + window.addEventListener('mouseup', handleMouseUp); + + return () => { + window.removeEventListener('mousemove', handleMouseMove); + window.removeEventListener('mouseup', handleMouseUp); + }; + }, [draggingNode, dragOffset]); + + // Update node heights when they mount or resize + useEffect(() => { + const updateHeights = () => { + const heights: Record = {}; + Object.entries(nodeRefs.current).forEach(([nodeId, ref]) => { + if (ref) { + heights[nodeId] = ref.offsetHeight; + } + }); + setNodeHeights(heights); + }; + + updateHeights(); + const resizeObserver = new ResizeObserver(updateHeights); + Object.values(nodeRefs.current).forEach(ref => { + if (ref) resizeObserver.observe(ref); + }); + + return () => resizeObserver.disconnect(); + }, [nodes]); + + // Calculate curve path for edge - connecting from right edge to left edge, vertically centered + const getCurvePath = (fromId: string, toId: string): string => { + const fromPos = nodePositions[fromId]; + const toPos = nodePositions[toId]; + + if (!fromPos || !toPos) return ''; + + const NODE_WIDTH = 110; + // Use actual node height or fallback to estimated height + const fromHeight = nodeHeights[fromId] || 50; + const toHeight = nodeHeights[toId] || 50; + + // Connect from right edge of source to left edge of target, vertically centered + const x1 = fromPos.x + NODE_WIDTH; + const y1 = fromPos.y + fromHeight / 2; + const x2 = toPos.x; + const y2 = toPos.y + toHeight / 2; + + // Calculate direction + const dx = x2 - x1; + + // Control points for smooth curve + // Use a smooth S-curve for horizontal connections + const controlOffset = Math.max(60, Math.abs(dx) * 0.4); + + // For horizontal connections, create a smooth S-curve + const cp1x = x1 + controlOffset; + const cp1y = y1; + const cp2x = x2 - controlOffset; + const cp2y = y2; + + return `M ${x1} ${y1} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${x2} ${y2}`; + }; + + + return ( + + {/* SVG for drawing curves */} + + {edges.map((edge, idx) => { + const path = getCurvePath(edge.from, edge.to); + if (!path) return null; + + return ( + + + + + + + + + ); + })} + + + {/* Nodes */} + + {nodes.map((node) => { + const isCurrent = currentStep === node.id; + const pos = nodePositions[node.id] || { x: 0, y: 0 }; + const isDragging = draggingNode === node.id; + + return ( + { + nodeRefs.current[node.id] = el; + }} + elevation={isCurrent ? 4 : isDragging ? 6 : 1} + onMouseDown={(e) => handleMouseDown(e, node.id)} + sx={{ + position: 'absolute', + left: pos.x, + top: pos.y, + p: 1, + width: 110, + borderRadius: 1.5, + border: isCurrent ? `2px solid ${theme.palette.primary.main}` : `1px solid ${alpha(theme.palette.divider, 0.5)}`, + backgroundColor: isCurrent + ? alpha(theme.palette.primary.main, 0.1) + : theme.palette.background.paper, + cursor: isDragging ? 'grabbing' : 'grab', + transition: isDragging ? 'none' : 'all 0.2s ease', + userSelect: 'none', + zIndex: isDragging ? 10 : 1, + '&:hover': { + transform: isDragging ? 'none' : 'translateY(-2px)', + boxShadow: theme.shadows[4], + }, + }} + > + + {node.name} + + {node.config && ( + + {node.config} + + )} + {isCurrent && ( + + )} + + ); + })} + + + ); +}; + // Recursive FileTreeView component interface TreeNode { folders: Record; diff --git a/webui/frontend/src/components/Layout.tsx b/webui/frontend/src/components/Layout.tsx index d2370a32c..9cc1a945c 100644 --- a/webui/frontend/src/components/Layout.tsx +++ b/webui/frontend/src/components/Layout.tsx @@ -18,6 +18,7 @@ import { } from '@mui/icons-material'; import { motion } from 'framer-motion'; import { useThemeContext } from '../context/ThemeContext'; +import { useSession } from '../context/SessionContext'; interface LayoutProps { children: ReactNode; @@ -29,6 +30,7 @@ interface LayoutProps { const Layout: React.FC = ({ children, onOpenSettings, onToggleLogs, showLogs }) => { const theme = useTheme(); const { mode, toggleTheme } = useThemeContext(); + const { clearSession, currentSession } = useSession(); return ( = ({ children, onOpenSettings, onToggleLogs, > = ({ children, onOpenSettings, onToggleLogs, alignItems: 'center', justifyContent: 'center', boxShadow: `0 4px 12px ${alpha(theme.palette.primary.main, 0.3)}`, + cursor: currentSession ? 'pointer' : 'default', + transition: 'transform 0.2s, box-shadow 0.2s', + '&:hover': currentSession ? { + transform: 'scale(1.05)', + boxShadow: `0 6px 16px ${alpha(theme.palette.primary.main, 0.4)}`, + } : {}, }} > = ({ children, onOpenSettings, onToggleLogs, color: theme.palette.text.secondary, }} > - © 2024 Alibaba Inc. + © 2026 Alibaba Inc. diff --git a/webui/frontend/src/components/LogViewer.tsx b/webui/frontend/src/components/LogViewer.tsx index 9961ddf67..cd37bb4ad 100644 --- a/webui/frontend/src/components/LogViewer.tsx +++ b/webui/frontend/src/components/LogViewer.tsx @@ -69,8 +69,11 @@ const LogViewer: React.FC = ({ logs, onClear }) => { width: 400, display: 'flex', flexDirection: 'column', + height: '100%', + maxHeight: '100%', backgroundColor: alpha(theme.palette.background.paper, 0.5), borderLeft: `1px solid ${theme.palette.divider}`, + overflow: 'hidden', }} > {/* Header */} @@ -142,14 +145,30 @@ const LogViewer: React.FC = ({ logs, onClear }) => { - {/* Log List */} + {/* Log List - Scrollable Container */} diff --git a/webui/frontend/src/components/SearchView.tsx b/webui/frontend/src/components/SearchView.tsx index ec724eafa..6b45d0a07 100644 --- a/webui/frontend/src/components/SearchView.tsx +++ b/webui/frontend/src/components/SearchView.tsx @@ -12,6 +12,8 @@ import { alpha, Grid, Tooltip, + ToggleButton, + ToggleButtonGroup, } from '@mui/material'; import { Search as SearchIcon, @@ -41,18 +43,26 @@ const SearchView: React.FC = () => { const [query, setQuery] = useState(''); const [selectedProject, setSelectedProject] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); + const [workflowType, setWorkflowType] = useState<'standard' | 'simple'>('standard'); const handleProjectSelect = (project: Project) => { setSelectedProject(project); + // Reset workflow type when switching projects + if (project.supports_workflow_switch) { + setWorkflowType('standard'); + } }; const handleSubmit = useCallback(async () => { if (!selectedProject || !query.trim()) return; - console.log('[SearchView] Submitting with project:', selectedProject.id, 'query:', query); + console.log('[SearchView] Submitting with project:', selectedProject.id, 'query:', query, 'workflow_type:', workflowType); setIsSubmitting(true); try { - const session = await createSession(selectedProject.id); + const session = await createSession( + selectedProject.id, + selectedProject.supports_workflow_switch ? workflowType : 'standard' + ); console.log('[SearchView] Session created:', session); if (session) { // Pass the session object directly to avoid race condition @@ -63,7 +73,7 @@ const SearchView: React.FC = () => { } finally { setIsSubmitting(false); } - }, [selectedProject, query, createSession, selectSession]); + }, [selectedProject, query, workflowType, createSession, selectSession]); const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { @@ -195,7 +205,7 @@ const SearchView: React.FC = () => { }} /> - {/* Selected Project Badge */} + {/* Selected Project Badge and Workflow Selector */} {selectedProject && ( { animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }} > - + } label={selectedProject.display_name} @@ -217,6 +227,80 @@ const SearchView: React.FC = () => { }} variant="outlined" /> + + {/* Workflow Type Selector for code_genesis */} + {selectedProject.supports_workflow_switch && ( + + + Select Workflow Type + + { + if (newValue !== null) { + setWorkflowType(newValue); + } + }} + size="small" + sx={{ + '& .MuiToggleButton-root': { + px: 2, + py: 0.5, + fontSize: '0.75rem', + borderColor: alpha(theme.palette.primary.main, 0.3), + '&.Mui-selected': { + backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, + '&:hover': { + backgroundColor: theme.palette.primary.dark, + }, + }, + }, + }} + > + + Standard Workflow + + + Simple Workflow + + + + {workflowType === 'standard' + ? 'Full design process: user story, architecture, file design, etc.' + : 'Simplified process: directly proceed to coding'} + + + )} )} diff --git a/webui/frontend/src/components/SettingsDialog.tsx b/webui/frontend/src/components/SettingsDialog.tsx index 6a273be81..4ed4f6728 100644 --- a/webui/frontend/src/components/SettingsDialog.tsx +++ b/webui/frontend/src/components/SettingsDialog.tsx @@ -22,6 +22,7 @@ import { Alert, Chip, Tooltip, + Autocomplete, } from '@mui/material'; import { Close as CloseIcon, @@ -44,6 +45,12 @@ interface LLMConfig { max_tokens: number; } +interface EditFileConfig { + api_key: string; + base_url: string; + diff_model: string; +} + interface MCPServer { type: 'stdio' | 'sse'; command?: string; @@ -75,6 +82,11 @@ const SettingsDialog: React.FC = ({ open, onClose }) => { temperature: 0.7, max_tokens: 4096, }); + const [editFileConfig, setEditFileConfig] = useState({ + api_key: '', + base_url: 'https://api.morphllm.com/v1', + diff_model: 'morph-v3-fast', + }); const [mcpServers, setMcpServers] = useState>({}); const [newServerName, setNewServerName] = useState(''); const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle'); @@ -88,13 +100,18 @@ const SettingsDialog: React.FC = ({ open, onClose }) => { const loadConfig = async () => { try { - const [llmRes, mcpRes] = await Promise.all([ + const [llmRes, mcpRes, editFileRes] = await Promise.all([ fetch('/api/config/llm'), fetch('/api/config/mcp'), + fetch('/api/config/edit_file'), ]); if (llmRes.ok) { const data = await llmRes.json(); + // Ensure temperature is between 0 and 1 + if (data.temperature !== undefined) { + data.temperature = Math.max(0, Math.min(1, data.temperature)); + } setLlmConfig(data); } @@ -102,6 +119,11 @@ const SettingsDialog: React.FC = ({ open, onClose }) => { const data = await mcpRes.json(); setMcpServers(data.mcpServers || {}); } + + if (editFileRes.ok) { + const data = await editFileRes.json(); + setEditFileConfig(data); + } } catch (error) { console.error('Failed to load config:', error); } @@ -122,7 +144,13 @@ const SettingsDialog: React.FC = ({ open, onClose }) => { body: JSON.stringify({ mcpServers: mcpServers }), }); - if (llmRes.ok && mcpRes.ok) { + const editFileRes = await fetch('/api/config/edit_file', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(editFileConfig), + }); + + if (llmRes.ok && mcpRes.ok && editFileRes.ok) { setSaveStatus('saved'); setTimeout(() => setSaveStatus('idle'), 2000); } else { @@ -231,30 +259,30 @@ const SettingsDialog: React.FC = ({ open, onClose }) => { - - Model - - - - {llmConfig.provider === 'custom' && ( - setLlmConfig((prev) => ({ ...prev, model: e.target.value }))} - /> - )} + { + // 只在用户输入时更新(不是选择时) + if (reason === 'input') { + setLlmConfig((prev) => ({ ...prev, model: newValue })); + } + }} + onChange={(_, newValue) => { + // 处理从下拉列表选择的情况 + const modelValue = typeof newValue === 'string' ? newValue : (newValue || ''); + setLlmConfig((prev) => ({ ...prev, model: modelValue })); + }} + renderInput={(params) => ( + + )} + /> = ({ open, onClose }) => { /> - Temperature: {llmConfig.temperature} + Temperature: {llmConfig.temperature.toFixed(1)} setLlmConfig((prev) => ({ ...prev, temperature: v as number }))} + onChange={(_, v) => { + const tempValue = v as number; + // Ensure temperature is between 0 and 1 + const clampedValue = Math.max(0, Math.min(1, tempValue)); + setLlmConfig((prev) => ({ ...prev, temperature: clampedValue })); + }} min={0} - max={2} + max={1} step={0.1} marks={[ { value: 0, label: '0' }, + { value: 0.5, label: '0.5' }, { value: 1, label: '1' }, - { value: 2, label: '2' }, ]} /> @@ -296,8 +329,58 @@ const SettingsDialog: React.FC = ({ open, onClose }) => { fullWidth label="Max Tokens" type="number" - value={llmConfig.max_tokens} - onChange={(e) => setLlmConfig((prev) => ({ ...prev, max_tokens: parseInt(e.target.value) || 4096 }))} + value={llmConfig.max_tokens || ''} + onChange={(e) => { + const value = e.target.value; + if (value === '') { + setLlmConfig((prev) => ({ ...prev, max_tokens: 0 })); + } else { + const numValue = parseInt(value, 10); + if (!isNaN(numValue) && numValue >= 0) { + setLlmConfig((prev) => ({ ...prev, max_tokens: numValue })); + } + } + }} + onBlur={(e) => { + // If empty on blur, set to default + if (e.target.value === '' || parseInt(e.target.value, 10) === 0) { + setLlmConfig((prev) => ({ ...prev, max_tokens: 4096 })); + } + }} + /> + + {/* Edit File Config Section */} + + + Edit File Configuration + + + Configure the API for the edit_file tool. If no API key is provided, the edit_file tool will be disabled. + + + setEditFileConfig((prev) => ({ ...prev, api_key: e.target.value }))} + helperText="API key for MorphLLM service (required to enable edit_file tool)" + /> + + setEditFileConfig((prev) => ({ ...prev, base_url: e.target.value }))} + helperText="Base URL for MorphLLM API" + /> + + setEditFileConfig((prev) => ({ ...prev, diff_model: e.target.value }))} + helperText="Model name for code diff generation (e.g., morph-v3-fast)" /> diff --git a/webui/frontend/src/context/SessionContext.tsx b/webui/frontend/src/context/SessionContext.tsx index ae3d5bc31..7ead15fc4 100644 --- a/webui/frontend/src/context/SessionContext.tsx +++ b/webui/frontend/src/context/SessionContext.tsx @@ -17,6 +17,7 @@ export interface Project { type: 'workflow' | 'agent' | 'script'; path: string; has_readme: boolean; + supports_workflow_switch?: boolean; } export interface WorkflowProgress { @@ -58,11 +59,12 @@ interface SessionContextType { isStreaming: boolean; isLoading: boolean; loadProjects: () => Promise; - createSession: (projectId: string) => Promise; + createSession: (projectId: string, workflowType?: string) => Promise; selectSession: (sessionId: string, initialQuery?: string, sessionObj?: Session) => void; sendMessage: (content: string) => void; stopAgent: () => void; clearLogs: () => void; + clearSession: () => void; } const SessionContext = createContext(undefined); @@ -118,12 +120,15 @@ export const SessionProvider: React.FC<{ children: ReactNode }> = ({ children }) }, []); // Create session - const createSession = useCallback(async (projectId: string): Promise => { + const createSession = useCallback(async (projectId: string, workflowType: string = 'standard'): Promise => { try { const response = await fetch(`${API_BASE}/sessions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ project_id: projectId }), + body: JSON.stringify({ + project_id: projectId, + workflow_type: workflowType + }), }); if (response.ok) { @@ -367,6 +372,19 @@ export const SessionProvider: React.FC<{ children: ReactNode }> = ({ children }) setLogs([]); }, []); + // Clear session (return to home) + const clearSession = useCallback(() => { + if (ws) { + ws.close(); + } + setCurrentSession(null); + setMessages([]); + setLogs([]); + setStreamingContent(''); + setIsLoading(false); + setIsStreaming(false); + }, [ws]); + // Initial load useEffect(() => { loadProjects(); @@ -398,6 +416,7 @@ export const SessionProvider: React.FC<{ children: ReactNode }> = ({ children }) sendMessage, stopAgent, clearLogs, + clearSession, }} > {children} diff --git a/webui/start.sh b/webui/start.sh index 9f77877fa..695b1cc74 100755 --- a/webui/start.sh +++ b/webui/start.sh @@ -31,11 +31,11 @@ else exit 1 fi -PY_VERSION=$($PYTHON_CMD -c "import sys; print(sys.version_info[:2] >= (3, 10))") -if [ "$PY_VERSION" != "True" ]; then - echo -e "${RED}Error: Python 3.10+ required.${NC}" - exit 1 -fi +# PY_VERSION=$($PYTHON_CMD -c "import sys; print(sys.version_info[:2] >= (3, 10))") +# if [ "$PY_VERSION" != "True" ]; then +# echo -e "${RED}Error: Python 3.10+ required.${NC}" +# exit 1 +# fi echo -e "${GREEN}Using Python: $PYTHON_CMD ($($PYTHON_CMD --version))${NC}" @@ -61,10 +61,9 @@ echo -e "${YELLOW}Installing Python dependencies...${NC}" pip install -q -r "$SCRIPT_DIR/requirements.txt" # Install ms-agent in development mode if not installed -if ! python -c "import ms_agent" 2>/dev/null; then - echo -e "${YELLOW}Installing ms-agent...${NC}" - pip install -q -e "$SCRIPT_DIR/../ms-agent" -fi +# Always reinstall to ensure entry point is correct +echo -e "${YELLOW}Installing/Updating ms-agent...${NC}" +pip install -q -e "$SCRIPT_DIR/.." # Install frontend dependencies if needed