PMFlow/backend/app/services/codegen.py

553 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Neural Sync - Code Draft Generation Service
AI 代码草案生成服务 - 激进策略
AI 尝试生成所有代码(包括复杂逻辑),但提供详细的 review points
"""
import asyncio
import os
import re
import subprocess
import logging
from typing import List, Optional, Dict, Any
from pydantic import BaseModel, Field
from datetime import datetime
from pathlib import Path
logger = logging.getLogger(__name__)
class CodeDraftRequest(BaseModel):
"""代码草案生成请求"""
signal_id: str
signal_content: str
strategic_analysis: Dict[str, Any]
target_files: List[str]
project_root: str = "."
class FileInfo(BaseModel):
"""文件信息"""
path: str
language: str
exists: bool
content: Optional[str] = None
lines_count: int = 0
class GeneratedFile(BaseModel):
"""生成的文件"""
path: str
content: str
language: str
diff: str
is_new: bool
review_points: List[str] = []
class CodeDraftResponse(BaseModel):
"""代码草案生成响应"""
success: bool
files: List[GeneratedFile]
summary: str
confidence: float = Field(default=0.0, ge=0.0, le=1.0)
warnings: List[str] = Field(default_factory=list)
review_points: List[str] = Field(default_factory=list)
estimated_complexity: str = "low"
generation_time: float = 0.0
metadata: Dict[str, Any] = Field(default_factory=dict)
class CodeDraftService:
"""
AI 代码草案生成服务 - 激进策略
特性:
- 支持全栈代码生成
- 读取现有代码上下文
- 生成 unified diff
- 提取人工 review 关键点
- 深度代码验证
"""
def __init__(self, llm_service):
self.llm = llm_service
self._language_map = {
".py": "python",
".ts": "typescript",
".tsx": "typescript",
".js": "javascript",
".jsx": "javascript",
".vue": "vue",
".sql": "sql",
".sh": "bash",
".yaml": "yaml",
".yml": "yaml",
".json": "json",
}
async def generate_draft(self, request: CodeDraftRequest) -> CodeDraftResponse:
"""
生成代码草案(激进策略)
"""
start_time = datetime.utcnow().timestamp()
try:
# 1. 读取目标文件
file_contexts = await self._read_target_files(request)
# 2. 分析代码库结构
repo_context = await self._analyze_repo_structure(request)
# 3. 构建激进策略的生成 prompt
prompt = self._build_aggressive_prompt(request, file_contexts, repo_context)
# 4. 调用 LLM 生成代码
llm_response = await self._call_llm_for_code(prompt)
# 5. 解析和验证生成的代码
generated_files = await self._parse_and_validate(
llm_response, file_contexts
)
# 6. 生成 unified diff
for file in generated_files:
file.diff = self._generate_diff(file_contexts, file)
# 7. 提取 review points
review_points = await self._extract_review_points(
generated_files, file_contexts, request
)
# 8. 计算置信度和复杂度
confidence = self._calculate_confidence(generated_files, file_contexts)
complexity = self._estimate_complexity(generated_files)
# 9. 生成摘要
summary = self._generate_summary(generated_files, request)
# 10. 收集警告
warnings = self._collect_warnings(generated_files, file_contexts)
generation_time = datetime.utcnow().timestamp() - start_time
return CodeDraftResponse(
success=True,
files=generated_files,
summary=summary,
confidence=confidence,
warnings=warnings,
review_points=review_points,
estimated_complexity=complexity,
generation_time=generation_time,
metadata={
"signal_id": request.signal_id,
"files_count": len(generated_files),
"new_files": sum(1 for f in generated_files if f.is_new),
"modified_files": sum(1 for f in generated_files if not f.is_new),
},
)
except Exception as e:
logger.error(f"Code generation failed: {e}", exc_info=True)
return CodeDraftResponse(
success=False,
files=[],
summary=f"代码生成失败: {str(e)}",
confidence=0.0,
warnings=[f"生成过程出错: {str(e)}"],
review_points=[],
generation_time=datetime.utcnow().timestamp() - start_time,
)
async def _read_target_files(
self, request: CodeDraftRequest
) -> Dict[str, FileInfo]:
"""读取目标文件内容"""
file_contexts = {}
for file_path in request.target_files:
full_path = os.path.join(request.project_root, file_path)
info = FileInfo(
path=file_path,
language=self._detect_language(file_path),
exists=os.path.exists(full_path),
)
if info.exists:
try:
with open(full_path, "r", encoding="utf-8") as f:
info.content = f.read()
info.lines_count = len(info.content.split("\n"))
except Exception as e:
logger.warning(f"Failed to read {file_path}: {e}")
info.content = None
file_contexts[file_path] = info
return file_contexts
def _detect_language(self, file_path: str) -> str:
"""检测文件语言"""
for ext, lang in self._language_map.items():
if file_path.endswith(ext):
return lang
return "text"
async def _analyze_repo_structure(self, request: CodeDraftRequest) -> Dict[str, Any]:
"""分析代码库结构"""
repo_context = {
"existing_modules": [],
"imports_available": [],
"patterns_used": [],
}
try:
# 扫描 Python 文件的导入
py_files = list(
Path(request.project_root).rglob("*.py")
)
for py_file in py_files[:20]: # 限制扫描数量
try:
content = py_file.read_text(encoding="utf-8", errors="ignore")
# 提取导入
imports = re.findall(r"^import (\w+)|^from (\w+) import", content, re.MULTILINE)
for imp in imports:
module = imp[0] or imp[1]
if module and module not in repo_context["imports_available"]:
repo_context["imports_available"].append(module)
except:
pass
except Exception as e:
logger.warning(f"Repo structure analysis failed: {e}")
return repo_context
def _build_aggressive_prompt(
self,
request: CodeDraftRequest,
file_contexts: Dict[str, FileInfo],
repo_context: Dict[str, Any],
) -> str:
"""构建激进的代码生成 prompt"""
prompt = f"""You are a senior full-stack engineer. Generate COMPLETE, PRODUCTION-READY code based on the following requirements.
## Signal (Requirement)
**Content**: {request.signal_content}
## Strategic Analysis
```json
{self._format_json(request.strategic_analysis)}
```
## Available Imports/Modules
{', '.join(repo_context['imports_available'][:20])}
## Existing Code Context
"""
for file_path, info in file_contexts.items():
prompt += f"\n### {file_path} ({info.language})\n"
if info.exists and info.content:
# 只显示关键部分(导入、类定义、函数签名)
lines = info.content.split("\n")
key_lines = []
for line in lines:
stripped = line.strip()
# 保留导入、类/函数定义、装饰器
if any(
stripped.startswith(p)
for p in ["import ", "from ", "class ", "def ", "@", "async def "]
):
key_lines.append(line)
elif len(key_lines) < 30: # 保留前30行作为上下文
key_lines.append(line)
prompt += "```" + info.language + "\n"
prompt += "\n".join(key_lines)
if len(lines) > 30:
prompt += f"\n... ({len(lines) - 30} more lines)\n"
prompt += "```\n"
else:
prompt += "*(New file to be created)*\n"
prompt += """
## Code Generation Requirements
1. **Complete Implementation**: Generate FULL, WORKING code including complex business logic
2. **Error Handling**: Proper exception handling and edge cases
3. **Performance**: Consider performance implications
4. **Type Safety**: Use proper type hints
5. **Comments**: Add concise comments for complex logic
6. **Testing**: Consider testability
## Output Format
Return a JSON response with this EXACT structure:
```json
{
"files": [
{
"path": "relative/path/to/file.ext",
"content": "full file content here",
"is_new": true/false,
"review_points": ["list of key points needing human review"]
}
],
"summary": "Brief description of changes",
"warnings": ["any potential issues or risks"]
}
```
IMPORTANT:
- Generate complete, runnable code
- Handle edge cases and errors
- Follow existing code patterns in the project
- Highlight complex logic in review_points
"""
return prompt
async def _call_llm_for_code(self, prompt: str) -> Dict[str, Any]:
"""调用 LLM 生成代码"""
try:
# 使用 LLM 服务的聊天接口
response = await self.llm._call_llm_chat(
messages=[
{"role": "system", "content": "You are an expert software engineer."},
{"role": "user", "content": prompt}
],
temperature=0.3, # 降低温度以获得更稳定的代码
)
# 解析 JSON 响应
json_match = re.search(r'```json\s*([\s\S]*?)\s*```', response)
if json_match:
import json
return json.loads(json_match.group(1))
# 尝试直接解析 JSON
import json
return json.loads(response)
except Exception as e:
logger.error(f"LLM call or parsing failed: {e}")
# 返回错误响应
return {
"files": [],
"summary": f"LLM 生成失败: {str(e)}",
"warnings": [str(e)]
}
async def _parse_and_validate(
self, llm_response: Dict[str, Any], file_contexts: Dict[str, FileInfo]
) -> List[GeneratedFile]:
"""解析和验证生成的代码"""
generated_files = []
for file_data in llm_response.get("files", []):
file_path = file_data.get("path", "")
content = file_data.get("content", "")
review_points = file_data.get("review_points", [])
file_info = file_contexts.get(file_path)
if not file_info:
file_info = FileInfo(
path=file_path,
language=self._detect_language(file_path),
exists=False,
)
# 基础验证
validation_warnings = self._validate_code_syntax(content, file_info.language)
if validation_warnings:
review_points.extend(validation_warnings)
generated_files.append(
GeneratedFile(
path=file_path,
content=content,
language=file_info.language,
diff="", # 稍后生成
is_new=not file_info.exists,
review_points=review_points,
)
)
return generated_files
def _validate_code_syntax(self, content: str, language: str) -> List[str]:
"""验证代码语法"""
warnings = []
try:
if language == "python":
import ast
ast.parse(content)
elif language in ["typescript", "javascript"]:
# 简单的括号匹配检查
if content.count("(") != content.count(")"):
warnings.append("括号不匹配")
if content.count("{") != content.count("}"):
warnings.append("花括号不匹配")
except SyntaxError as e:
warnings.append(f"语法错误: {str(e)}")
return warnings
def _generate_diff(
self, file_contexts: Dict[str, FileInfo], file: GeneratedFile
) -> str:
"""生成 unified diff"""
if file.is_new:
return f"--- /dev/null\n+++ {file.path}\n@@ -0,0 +1,{file.content.count(chr(10))} @@\n" + "\n".join(
f"+{line}" for line in file.content.split("\n")
)
# 对于修改的文件
old_content = file_contexts.get(file.path)?.content or ""
old_lines = old_content.split("\n")
new_lines = file.content.split("\n")
# 简化的 diff 生成(实际应该使用 difflib)
diff_lines = [f"--- {file.path}", f"+++ {file.path}"]
# 找出差异的行范围
changed_ranges = self._find_changed_ranges(old_lines, new_lines)
for start_old, start_new, length in changed_ranges:
diff_lines.append(f"@@ -{start_old + 1},{length} +{start_new + 1},{length} @@")
for i in range(length):
old_line = old_lines[start_old + i] if start_old + i < len(old_lines) else ""
new_line = new_lines[start_new + i] if start_new + i < len(new_lines) else ""
if old_line == new_line:
diff_lines.append(f" {new_line}")
elif new_line and not old_line:
diff_lines.append(f"+{new_line}")
elif old_line and not new_line:
diff_lines.append(f"-{old_line}")
else:
diff_lines.append(f"-{old_line}")
diff_lines.append(f"+{new_line}")
return "\n".join(diff_lines)
def _find_changed_ranges(
self, old_lines: List[str], new_lines: List[str]
) -> List[tuple]:
"""找出变化的代码行范围"""
# 简化实现:假设整个文件是变化的
return [(0, 0, max(len(old_lines), len(new_lines)))]
async def _extract_review_points(
self, generated_files: List[GeneratedFile],
file_contexts: Dict[str, FileInfo],
request: CodeDraftRequest,
) -> List[str]:
"""提取需要人工 review 的关键点"""
review_points = []
# 收集所有文件的 review points
for file in generated_files:
review_points.extend(file.review_points)
# 添加通用 review points
review_points.extend([
"验证生成的代码是否符合项目的编码规范",
"检查是否有安全漏洞(SQL注入、XSS等)",
"确认复杂业务逻辑的正确性",
"验证错误处理是否完整",
"检查是否有性能优化空间",
])
# 根据战略分析添加特定 review points
strategic_tags = request.strategic_analysis.get("decision_proposal", {}).get("tags", [])
if "security" in strategic_tags:
review_points.append("重点关注安全相关代码")
if "performance" in strategic_tags:
review_points.append("验证性能指标是否达标")
return list(set(review_points)) # 去重
def _calculate_confidence(
self, generated_files: List[GeneratedFile], file_contexts: Dict[str, FileInfo]
) -> float:
"""计算生成置信度"""
if not generated_files:
return 0.0
confidence = 0.7 # 基础置信度
# 根据文件数量调整
confidence -= min(len(generated_files) * 0.05, 0.2)
# 根据警告数量调整
total_warnings = sum(len(f.review_points) for f in generated_files)
confidence -= min(total_warnings * 0.02, 0.3)
return max(0.0, min(1.0, confidence))
def _estimate_complexity(self, generated_files: List[GeneratedFile]) -> str:
"""估算代码复杂度"""
total_lines = sum(len(f.content.split("\n")) for f in generated_files)
if total_lines < 100:
return "low"
elif total_lines < 300:
return "medium"
else:
return "high"
def _generate_summary(
self, generated_files: List[GeneratedFile], request: CodeDraftRequest
) -> str:
"""生成变更摘要"""
new_files = [f for f in generated_files if f.is_new]
modified_files = [f for f in generated_files if not f.is_new]
summary_parts = [
f"为信号 '{request.signal_content[:50]}...' 生成代码草案",
f"新建文件: {len(new_files)} 个",
f"修改文件: {len(modified_files)} 个",
]
if modified_files:
summary_parts.append(f"主要变更: {modified_files[0].path}")
return " | ".join(summary_parts)
def _collect_warnings(
self, generated_files: List[GeneratedFile], file_contexts: Dict[str, FileInfo]
) -> List[str]:
"""收集警告信息"""
warnings = []
for file in generated_files:
if not file.content.strip():
warnings.append(f"{file.path}: 文件内容为空")
# 检查是否有 TODO/FIXME
if "TODO" in file.content or "FIXME" in file.content:
warnings.append(f"{file.path}: 包含未完成的标记")
return warnings
def _format_json(self, obj: Any) -> str:
"""格式化 JSON"""
import json
return json.dumps(obj, ensure_ascii=False, indent=2)
# 全局服务实例
_codegen_service = None
def get_codegen_service(llm_service) -> CodeDraftService:
"""获取代码生成服务实例"""
global _codegen_service
if _codegen_service is None:
_codegen_service = CodeDraftService(llm_service)
return _codegen_service