add /chatflow.ask and /chatflow.gen

This commit is contained in:
bobo.yang 2025-03-11 13:29:58 +08:00
parent 1c0e5e045f
commit 344fbff719
8 changed files with 685 additions and 0 deletions

View File

@ -0,0 +1,42 @@
# 在 ask/command.py 中
import os
import sys
from devchat.llm import chat
from lib.ide_service import IDEService
ROOT_WORKFLOW_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.append(ROOT_WORKFLOW_DIR)
from chatflow.util.contexts import CONTEXTS
PROMPT = f"""
{CONTEXTS.replace("{", "{{").replace("}", "}}")}
""" + """
当前选中代码{selected_code}
当前打开文件路径{file_path}
用户要求或问题{question}
"""
@chat(prompt=PROMPT, stream_out=True)
def ask(question, selected_code, file_path):
pass
def get_selected_code():
"""Retrieves the selected lines of code from the user's selection."""
selected_data = IDEService().get_selected_range().dict()
return selected_data
def main(question):
selected_text = get_selected_code()
file_path = selected_text.get("abspath", "")
code_text = selected_text.get("text", "")
ask(question=question, selected_code=code_text, file_path=file_path)
sys.exit(0)
if __name__ == "__main__":
main(sys.argv[1])

View File

@ -0,0 +1,4 @@
description: 生成工作流命令,输入命令信息
input: required
steps:
- run: $devchat_python $command_path/command.py "$input"

View File

@ -0,0 +1,421 @@
"""
生成工作流命令实现
步骤1: 根据用户输入的工作流命令定义信息生成相关工作流实现步骤描述展示相关信息等待用户确认
步骤2: 根据用户确认的工作流实现步骤描述生成工作流命令实现代码并保存到指定文件中
"""
#!/usr/bin/env python3
import os
import sys
import re
import yaml
import subprocess
from pathlib import Path
from lib.chatmark import Button, Form, TextEditor, Step, Radio, Checkbox
from lib.ide_service import IDEService
from devchat.llm import chat, chat_json
ROOT_WORKFLOW_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.append(ROOT_WORKFLOW_DIR)
from chatflow.util.contexts import CONTEXTS
# 工作流命令定义模板
COMMAND_YML_TEMPLATE = """description: {description}
{input_section}
{help_section}{workflow_python_section}
steps:
- run: ${python_cmd} $command_path/command.py{input_param}
"""
# 工作流命令实现模板
COMMAND_PY_TEMPLATE = """#!/usr/bin/env python3
import os
import sys
from pathlib import Path
{imports}
def main():
{main_code}
if __name__ == "__main__":
main()
"""
# 从用户输入提取工作流信息的提示
EXTRACT_INFO_PROMPT = """
请从以下用户输入中提取创建工作流命令所需的关键信息
用户输入:
{user_input}
工作流上下文信息:
{contexts}
请提取以下信息并以JSON格式返回
1. command_name: 工作流命令名称应以/开头"/example""/category.command"
2. description: 工作流命令的简短描述
3. input_required: 工作流是否需要输入参数(true/false)
4. custom_env: 是否需要自定义Python环境(true/false)
5. env_name: 如果需要自定义环境环境名称是什么
6. dependencies: 如果需要自定义环境依赖文件名是什么(如requirements.txt)
7. purpose: 工作流的主要目的和功能
8. implementation_ideas: 实现思路的简要描述
如果某项信息在用户输入中未明确指定请根据上下文合理推断
返回格式示例:
{{
"command_name": "/example.command",
"description": "这是一个示例命令",
"input_required": true,
"custom_env": false,
"env_name": "",
"dependencies": "",
"purpose": "这个命令的主要目的是...",
"implementation_ideas": "可以通过以下步骤实现..."
}}
"""
# 工作流步骤生成提示
WORKFLOW_STEPS_PROMPT = """
请为以下工作流命令对应脚本文件生成详细的实现步骤描述
工作流命令名称: {command_name}
工作流命令描述: {description}
输入要求: {input_requirement}
工作流目的: {purpose}
实现思路: {implementation_ideas}
工作流上下文信息:
{contexts}
请提供清晰的步骤描述包括
1. 每个步骤需要完成的具体任务
2. 每个步骤可能需要的输入和输出
3. 每个步骤可能需要使用的IDE Service或ChatMark组件
4. 任何其他实现细节
返回格式应为markdown块包裹的步骤列表每个步骤都有详细描述输出示例如下
```steps
步骤1: 获取用户输入的重构任务要求
步骤2: 调用IDE Service获取选中代码
步骤3: 根据用户重构任务要求调用大模型生成选中代码的重构代码
步骤4: 调用IDE Service将生成的重构代码通过DIFF VIEW方式展示给用户
```
不要输出工作流命令的其他构建步骤只需要清洗描述工作流命令对应command.py中对应的步骤实现即可
"""
# 代码实现生成提示
CODE_IMPLEMENTATION_PROMPT = """
请为以下工作流命令生成Python实现代码
工作流命令名称: {command_name}
工作流命令描述: {description}
输入要求: {input_requirement}
自定义环境: {custom_env}
工作流实现步骤:
{workflow_steps}
工作流上下文信息:
{contexts}
请生成完整的Python代码实现包括
1. 必要的导入语句
2. 主函数实现
3. 按照工作流步骤实现具体功能
4. 适当的错误处理
5. 必要的注释说明
6. 所有markdown代码块都要有明确的语言标识如pythonjsonyamlcode等
代码应该使用IDE Service接口与IDE交互使用ChatMark组件与用户交互
输出格式应为markdown代码块语言标识为python仅输出工作流实现对应的脚本代码块不需要输出其他逻辑信息
只需要输出最终PYTHON代码块不需要其他信息例如
```python
....
```
"""
@chat_json(prompt=EXTRACT_INFO_PROMPT)
def extract_workflow_info(user_input, contexts):
"""从用户输入中提取工作流信息"""
pass
@chat(prompt=WORKFLOW_STEPS_PROMPT, stream_out=False)
def generate_workflow_steps(command_name, description, input_requirement, purpose, implementation_ideas, contexts):
"""生成工作流实现步骤描述"""
pass
@chat(prompt=CODE_IMPLEMENTATION_PROMPT, stream_out=False)
def generate_workflow_code(command_name, description, input_requirement, custom_env, workflow_steps, contexts):
"""生成工作流实现代码"""
pass
def parse_command_path(command_name):
"""
解析命令路径返回目录结构和最终命令名
确保工作流创建在custom目录下的有效namespace中
"""
parts = command_name.strip('/').split('.')
# 获取custom目录路径
custom_dir = os.path.join(os.path.expanduser('~'), '.chat', 'scripts', 'custom')
# 获取custom目录下的有效namespace
valid_namespaces = []
config_path = os.path.join(custom_dir, 'config.yml')
if os.path.exists(config_path):
try:
with open(config_path, 'r') as f:
config = yaml.safe_load(f)
if config and 'namespaces' in config:
valid_namespaces = config['namespaces']
except Exception as e:
print(f"读取custom配置文件失败: {str(e)}")
# 如果没有找到有效namespace使用默认namespace
if not valid_namespaces:
print("警告: 未找到有效的custom namespace将使用默认namespace 'default'")
valid_namespaces = ['default']
# 确保default namespace存在于config.yml中
try:
os.makedirs(os.path.dirname(config_path), exist_ok=True)
with open(config_path, 'w') as f:
yaml.dump({'namespaces': ['default']}, f)
except Exception as e:
print(f"创建默认namespace配置失败: {str(e)}")
# 使用第一个有效namespace
namespace = valid_namespaces[0]
# 创建最终命令目录路径
command_dir = os.path.join(custom_dir, namespace)
# 创建目录结构
for part in parts[:-1]:
command_dir = os.path.join(command_dir, part)
return command_dir, parts[-1]
def parse_markdown_block(response, block_type="steps"):
"""
从AI响应中解析指定类型的Markdown代码块内容支持处理嵌套的代码块
Args:
response (str): AI生成的响应文本
block_type (str): 要解析的代码块类型默认为"steps"
Returns:
str: 解析出的代码块内容
Raises:
Exception: 解析失败时抛出异常
"""
try:
# 处理可能存在的思考过程
if response.find("</think>") != -1:
response = response.split("</think>")[-1]
# 构建起始标记
start_marker = f"```{block_type}"
end_marker = "```"
# 查找起始位置
start_pos = response.find(start_marker)
if start_pos == -1:
# 如果没有找到指定类型的标记,直接返回原文本
return response.strip()
# 从标记后开始的位置
content_start = start_pos + len(start_marker)
# 从content_start开始找到第一个未配对的```
pos = content_start
open_blocks = 1 # 已经有一个开放的块
while True:
# 找到下一个```
next_marker = response.find(end_marker, pos)
if next_marker == -1:
# 如果没有找到结束标记,返回剩余所有内容
break
# 检查这是开始还是结束标记
# 向后看是否跟着语言标识符
after_marker = response[next_marker + 3:]
# 检查是否是新的代码块开始 - 只要```后面跟着非空白字符,就认为是新代码块开始
if after_marker.strip() and not after_marker.startswith("\n"):
first_word = after_marker.split()[0]
if not any(c in first_word for c in ",.;:!?()[]{}"):
open_blocks += 1
else:
open_blocks -= 1
else:
open_blocks -= 1
if open_blocks == 0:
# 找到匹配的结束标记
return response[content_start:next_marker].strip()
pos = next_marker + 3
# 如果没有找到匹配的结束标记返回从content_start到末尾的内容
return response[content_start:].strip()
except Exception as e:
import logging
logging.info(f"Response: {response}")
logging.error(f"Exception in parse_markdown_block: {str(e)}")
raise Exception(f"解析{block_type}内容失败: {str(e)}") from e
def create_workflow_files(command_dir, command_name, description, input_required,
code):
"""创建工作流命令文件"""
# 创建命令目录
os.makedirs(command_dir, exist_ok=True)
# 创建command.yml
input_section = f"input: {'required' if input_required else 'optional'}"
help_section = "help: README.md"
input_param = ' "$input"' if input_required else ''
# 添加自定义环境配置
workflow_python_section = ""
python_cmd = "devchat_python"
yml_content = COMMAND_YML_TEMPLATE.format(
description=description,
input_section=input_section,
help_section=help_section,
workflow_python_section=workflow_python_section,
python_cmd=python_cmd,
input_param=input_param
)
with open(os.path.join(command_dir, 'command.yml'), 'w') as f:
f.write(yml_content)
# 创建command.py
with open(os.path.join(command_dir, 'command.py'), 'w') as f:
f.write(code)
# 设置执行权限
os.chmod(os.path.join(command_dir, 'command.py'), 0o755)
# 创建README.md
readme_content = f"# {command_name}\n\n{description}\n"
with open(os.path.join(command_dir, 'README.md'), 'w') as f:
f.write(readme_content)
def main():
# 获取用户输入
user_input = sys.argv[1] if len(sys.argv) > 1 else ""
# 步骤1: 通过AI分析用户输入提取必要信息
with Step("分析用户输入,提取工作流信息..."):
workflow_info = extract_workflow_info(user_input=user_input, contexts=CONTEXTS)
# 步骤3: 生成工作流实现步骤描述
with Step("生成工作流实现步骤描述..."):
workflow_steps = generate_workflow_steps(
command_name=workflow_info.get("command_name", ""),
description=workflow_info.get("description", ""),
input_requirement="可选",
purpose=workflow_info.get("purpose", ""),
implementation_ideas=workflow_info.get("implementation_ideas", ""),
contexts=CONTEXTS
)
workflow_steps = parse_markdown_block(workflow_steps, block_type="steps")
# 步骤2: 使用Form组件一次性展示所有信息让用户编辑
print("\n## 工作流信息\n")
# 创建所有编辑组件
command_name_editor = TextEditor(workflow_info.get("command_name", "/example.command"))
description_editor = TextEditor(workflow_info.get("description", "工作流命令描述"))
input_radio = Radio(["必选 (required)", "可选 (optional)"])
purpose_editor = TextEditor(workflow_info.get("purpose", "请描述工作流的主要目的和功能"))
# ideas_editor = TextEditor(workflow_info.get("implementation_ideas", "请描述实现思路"))
steps_editor = TextEditor(workflow_steps)
# 使用Form组件一次性展示所有编辑组件
form = Form([
"命令名称:",
command_name_editor,
"输入要求:",
input_radio,
"描述:",
description_editor,
"工作流目的:",
purpose_editor,
"实现步骤:",
steps_editor
])
form.render()
# 获取用户编辑后的值
command_name = command_name_editor.new_text
description = description_editor.new_text
input_required = input_radio.selection == 0
purpose = purpose_editor.new_text
# implementation_ideas = ideas_editor.new_text
workflow_steps = steps_editor.new_text
# 步骤4: 生成工作流实现代码
with Step("生成工作流实现代码..."):
code = generate_workflow_code(
command_name=command_name,
description=description,
input_requirement="必选" if input_required else "可选",
custom_env=False,
workflow_steps=workflow_steps,
contexts=CONTEXTS
)
code = code.strip()
start_index = code.find("```python")
end_index = code.rfind("```")
code = code[start_index + len("```python"):end_index].strip()
# code = parse_markdown_block(code, block_type="python")
# 解析命令路径
command_dir, final_command = parse_command_path(command_name)
full_command_dir = os.path.join(command_dir, final_command)
# 步骤5: 创建工作流文件
with Step(f"创建工作流文件到 {full_command_dir}"):
create_workflow_files(
full_command_dir,
command_name,
description,
input_required,
code
)
# 更新斜杠命令
with Step("更新斜杠命令..."):
IDEService().update_slash_commands()
# 在新窗口中打开工作流目录
try:
subprocess.run(["code", full_command_dir], check=True)
except subprocess.SubprocessError:
print(f"无法自动打开编辑器,请手动打开工作流目录: {full_command_dir}")
print(f"\n✅ 工作流命令 {command_name} 已成功创建!")
print(f"命令文件位置: {full_command_dir}")
print("你现在可以在DevChat中使用这个命令了。")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,4 @@
description: 生成工作流命令,输入命令信息
input: required
steps:
- run: $devchat_python $command_path/command.py "$input"

View File

@ -0,0 +1,64 @@
工作流开发中封装了部分基础函数,方便开发者在工作流中使用。
**大模型调用**
针对大模型调用封装了几个装饰器函数分别用于调用大模型生成文本和生成json格式的文本。
- chat装饰器
用于调用大模型生成文本,并将生成的文本返回给调用者。具体使用示例如下:
```python
from devchat.llm import chat
PROMPT = """
对以下代码段进行解释:
{code}
"""
@chat(prompt=PROMPT, stream_out=True)
# pylint: disable=unused-argument
def explain(code):
"""
call ai to explain selected code
"""
pass
ai_explanation = explain(code="def foo(): pass")
```
调用explain函数时需要使用参数名称=参数值的方式传递参数参数名称必须与PROMPT中使用的参数名称一致。
- chat_json装饰器
用于调用大模型生成json对象。具体使用示例如下
```python
from devchat.llm import chat_json
PROMPT = (
"Give me 5 different git branch names, "
"mainly hoping to express: {task}, "
"Good branch name should looks like: <type>/<main content>,"
"the final result is output in JSON format, "
'as follows: {{"names":["name1", "name2", .. "name5"]}}\n'
)
@chat_json(prompt=PROMPT)
def generate_branch_name(task):
pass
task = "fix bug"
branch_names = generate_branch_name(task=task)
print(branch_names["names"])
```
调用generate_branch_name函数时需要使用参数名称=参数值的方式传递参数参数名称必须与PROMPT中使用的参数名称一致。
使用chat, chat_json的注意
1. 使用chat_json装饰器时返回的结果是一个字典对象在PROMPT中描述这个字典对象的结构时需要使用{{}}来表示{}。因为后续操作中会使用类似f-string的方式替代PROMPT中的参数所以在PROMPT中使用{}时需要使用{{}}来表示{}。
2. 使用这两个装饰器时PROMPT中不要使用markdown的代码块语法。
**工作流调用**
目的是对已有工作流进行复用在A工作流中调用B工作流。
- workflow_call函数
调用指定的工作流,并将指定的参数传递给被调用的工作流。具体使用示例如下:
```python
from lib.workflow import workflow_call
ret_code = workflow_call("/helloworld some name")
if ret_code == 0:
print("workflow call success")
else:
print("workflow call failed")
```

View File

@ -0,0 +1,93 @@
"""
为workflow命令提供上下文信息
编写工作流实现代码需要的上下文
1. IDE Service接口定义
2. ChatMark接口定义
3. 工作流命令列表
4. 工作流命令组织实现规范
5. 基础可用的函数信息
"""
import os
from typing import List
def load_file_in_user_scripts(filename: str) -> str:
"""
从用户脚本目录中加载文件内容
"""
user_path = os.path.expanduser("~/.chat/scripts")
file_path = os.path.join(user_path, filename)
with open(file_path, "r") as f:
return f.read()
def load_local_file(filename: str) -> str:
"""
从当前脚本所在目录的相对目录加载文件内容
"""
script_dir = os.path.dirname(os.path.abspath(__file__))
file_path = os.path.join(script_dir, filename)
with open(file_path, "r") as f:
return f.read()
def load_existing_workflow_defines() -> str:
""" 从user scripts目录遍历找到所有command.yml文件并加载其内容 """
merico_path = os.path.expanduser("~/.chat/scripts/merico")
community_path = os.path.expanduser("~/.chat/scripts/community")
custom_path = os.path.expanduser("~/.chat/scripts/custom")
root_paths = [merico_path]
# 遍历community_path、custom_path下直接子目录将其添加到root_paths作为下一步的根目录
for path in [community_path, custom_path]:
for root, dirs, files in os.walk(path):
if root == path:
root_paths.extend([os.path.join(root, d) for d in dirs])
break
wrkflow_defines = []
# 遍历所有根目录对每个根目录进行递归遍历找到所有command.yml文件并加载其内容
# 将目录名称与command.yml内容拼接成一个字符串添加到wrkflow_defines列表中
# 例如:~/.chat/scripts/merico/github/commit/command.yml被找到那么拼接的字符串为
# 工作流命令/github.commit的定义\n<command.yml内容>\n\n
for root_path in root_paths:
for root, dirs, files in os.walk(root_path):
if "command.yml" in files:
with open(os.path.join(root, "command.yml"), "r") as f:
wrkflow_defines.append(f"工作流命令/{root[len(root_path)+1:].replace(os.sep, '.')}的定义:\n{f.read()}\n\n")
return "\n".join(wrkflow_defines)
CONTEXTS = f"""
工作流开发需要的上下文信息
# IDE Service接口定义及使用示例
IDE Service用于在工作流命令中访问与IDE相关的数据以及调用IDE提供的功能
## IDEService接口定义
接口定义
{load_file_in_user_scripts("lib/ide_service/service.py")}
涉及类型定义
{load_file_in_user_scripts("lib/ide_service/types.py")}
## IDEService接口示例
{load_local_file("ide_service_demo.py")}
# ChatMark接口使用示例
ChatMark用于在工作流命令中与用户交互展示信息获取用户输入
{load_file_in_user_scripts("lib/chatmark/chatmark_example/main.py")}
ChatMark Form组件类似于HTML表单用于组合多个组件获取相关设置结果
# 工作流命令规范
{load_local_file("workflow_guide.md")}
# 已有工作流命令定义
{load_existing_workflow_defines()}
# 工作流内部函数定义
{load_local_file("base_functions_guide.md")}
"""

View File

@ -0,0 +1,5 @@
from lib.ide_service import IDEService
IDEService().ide_logging(
"debug", "Hello IDE Service!"
)

View File

@ -0,0 +1,52 @@
一个工作流对应一个目录目录可以进行嵌套每个工作流在UI视图中对应了一个斜杠命令例如/github.commit。
github工作流目录结构简化后示意如下
github
-- commit
|-- command.yml
|-- command.py
|-- README.md
-- new_pr
|-- command.yml
|-- command.py
......
"command.yml"文件定义了工作流命令的元信息,例如命令的名称、描述、参数等。
"command.py"文件定义了工作流命令的实现逻辑。
拿一个hello world工作流命令为例目录结构如下
helloworld
-- command.yml
-- command.py
-- README.md
"command.yml"文件内容如下:
```yaml
description: Hello Workflow, use as /helloworld <name>
input: required
steps:
- run: $devchat_python $command_path/command.py "$input"
````
"command.py"文件内容如下:
```python
import sys
print(sys.argv[1])
```
使用一个工作流时在DevChat AI智能问答视图中输入斜杠命令例如/helloworld <name>,即可执行该工作流命令。/helloworld表示对应系统可用的工作流。<name>表示工作流的输入是可选的如果工作流定义中input为required则必须输入否则可以不输入。
工作流命令有重载优先级merico目录下工作流 < community目录下工作流 < custom目录下工作流
例如如果merico与community目录下都有github工作流那么在DevChat AI智能问答视图中输入/github命令时会优先执行community目录下的github工作流命令。
在custom目录下通过config.yml文件定义custom目录下的工作流目录。例如
namespaces:
- bobo
那么custom/bobo就是一个工作流存储目录可以在该目录下定义工作流命令。
例如:
custom/bobo
| ---- hello
|------ command.yml
|------ command.py
那么custom/bobo/hello对应的工作流命令就是/hello.