1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
| @register_tool(include_functions=["run"])
class WritePRD(Action):
"""WritePRD处理以下情况:
1. 缺陷修复:如果需求是缺陷修复,将生成缺陷文档
2. 新需求:如果需求是新需求,将生成PRD文档
3. 需求更新:如果需求是更新,将更新PRD文档
"""
repo: Optional[ProjectRepo] = Field(default=None, exclude=True)
input_args: Optional[BaseModel] = Field(default=None, exclude=True)
async def run(
self,
with_messages: List[Message] = None,
*,
user_requirement: str = "",
output_pathname: str = "",
legacy_prd_filename: str = "",
extra_info: str = "",
**kwargs,
) -> Union[AIMessage, str]:
"""运行PRD生成流程"""
# API调用模式
if not with_messages:
return await self._execute_api(
user_requirement=user_requirement,
output_pathname=output_pathname,
legacy_prd_filename=legacy_prd_filename,
extra_info=extra_info,
)
# 消息驱动模式
self.input_args = with_messages[-1].instruct_content
if not self.input_args:
# 初始化项目仓库
self.repo = ProjectRepo(self.context.kwargs.project_path)
await self.repo.docs.save(filename=REQUIREMENT_FILENAME, content=with_messages[-1].content)
# 创建指令内容
self.input_args = AIMessage.create_instruct_value(
kvs={
"project_path": self.context.kwargs.project_path,
"requirements_filename": str(self.repo.docs.workdir / REQUIREMENT_FILENAME),
"prd_filenames": [str(self.repo.docs.prd.workdir / i) for i in self.repo.docs.prd.all_files],
},
class_name="PrepareDocumentsOutput",
)
else:
self.repo = ProjectRepo(self.input_args.project_path)
# 加载需求文档
req = await Document.load(filename=self.input_args.requirements_filename)
docs: list[Document] = [
await Document.load(filename=i, project_path=self.repo.workdir)
for i in self.input_args.prd_filenames
]
if not req:
raise FileNotFoundError("No requirement document found.")
# 判断需求类型并处理
if await self._is_bugfix(req.content):
logger.info(f"Bugfix detected: {req.content}")
return await self._handle_bugfix(req)
# 移除上一轮的缺陷文件以避免冲突
await self.repo.docs.delete(filename=BUGFIX_FILENAME)
# 如果需求与其他文档相关,更新它们,否则创建新文档
if related_docs := await self.get_related_docs(req, docs):
logger.info(f"Requirement update detected: {req.content}")
await self._handle_requirement_update(req=req, related_docs=related_docs)
else:
logger.info(f"New requirement detected: {req.content}")
await self._handle_new_requirement(req)
# 构建返回结果
kvs = self.input_args.model_dump()
kvs["changed_prd_filenames"] = [
str(self.repo.docs.prd.workdir / i) for i in list(self.repo.docs.prd.changed_files.keys())
]
kvs["project_path"] = str(self.repo.workdir)
kvs["requirements_filename"] = str(self.repo.docs.workdir / REQUIREMENT_FILENAME)
self.context.kwargs.project_path = str(self.repo.workdir)
return AIMessage(
content="PRD is completed. " + "\n".join(
list(self.repo.docs.prd.changed_files.keys())
+ list(self.repo.resources.prd.changed_files.keys())
+ list(self.repo.resources.competitive_analysis.changed_files.keys())
),
instruct_content=AIMessage.create_instruct_value(kvs=kvs, class_name="WritePRDOutput"),
cause_by=self,
)
async def _new_prd(self, requirement: str) -> ActionNode:
"""生成新的PRD"""
project_name = self.project_name
context = CONTEXT_TEMPLATE.format(requirements=requirement, project_name=project_name)
exclude = [PROJECT_NAME.key] if project_name else []
node = await WRITE_PRD_NODE.fill(
req=context, llm=self.llm, exclude=exclude, schema=self.prompt_schema
)
return node
async def _handle_new_requirement(self, req: Document) -> ActionOutput:
"""处理新需求"""
async with DocsReporter(enable_llm_stream=True) as reporter:
await reporter.async_report({"type": "prd"}, "meta")
node = await self._new_prd(req.content)
await self._rename_workspace(node)
new_prd_doc = await self.repo.docs.prd.save(
filename=FileRepository.new_filename() + ".json",
content=node.instruct_content.model_dump_json()
)
await self._save_competitive_analysis(new_prd_doc)
md = await self.repo.resources.prd.save_pdf(doc=new_prd_doc)
await reporter.async_report(self.repo.workdir / md.root_relative_path, "path")
return Documents.from_iterable(documents=[new_prd_doc]).to_action_output()
async def _is_bugfix(self, context: str) -> bool:
"""判断是否为缺陷修复"""
if not self.repo.code_files_exists():
return False
node = await WP_ISSUE_TYPE_NODE.fill(req=context, llm=self.llm)
return node.get("issue_type") == "BUG"
async def get_related_docs(self, req: Document, docs: list[Document]) -> list[Document]:
"""获取相关文档"""
return [i for i in docs if await self._is_related(req, i)]
async def _is_related(self, req: Document, old_prd: Document) -> bool:
"""判断需求是否与现有PRD相关"""
context = NEW_REQ_TEMPLATE.format(old_prd=old_prd.content, requirements=req.content)
node = await WP_IS_RELATIVE_NODE.fill(req=context, llm=self.llm)
return node.get("is_relative") == "YES"
|