init the project

This commit is contained in:
evyzacq
2026-05-25 15:09:42 +08:00
commit 7fc0e7852e
122 changed files with 14557 additions and 0 deletions
+200
View File
@@ -0,0 +1,200 @@
"""
Stage 1: Semantic Index Generation.
Reads the full parsed PRD JSON, calls the LLM once to produce a semantic index
identifying all function units, concepts, and their document sources.
Output: output/semantic_index.json
"""
import json
import re
import sys
import time
from pathlib import Path
import config
def format_document_for_prompt(doc: dict) -> str:
"""Render the full parsed document as a readable string for the LLM prompt."""
lines = []
# ---- Sections ----
lines.append("=== SECTIONS ===")
for i, section in enumerate(doc.get("sections", [])):
source = section.get("source", f"(无标题-章节{i})")
lines.append(f"\n--- Section: {source} ---")
for block in section.get("blocks", []):
if block["type"] == "para":
lines.append(f"[段落 {block['index']}] {block['text']}")
elif block["type"] == "table":
lines.append(f"[表格 {block.get('table', '?')}]")
headers = block.get("headers", [])
lines.append(f" 表头: {' | '.join(headers)}")
for row in block.get("rows", []):
cols = row.get("columns", [])
cell_texts = []
for c in cols:
cell_texts.append(f"[行{c.get('row','?')}]{c.get('name','')}: {c.get('text','')}")
lines.append(f" {'; '.join(cell_texts)}")
images = section.get("images", [])
if images:
lines.append(f" 图片引用: {', '.join(images)}")
# ---- Image Analysis ----
lines.append("\n\n=== IMAGE_ANALYSIS (流程图逻辑树) ===")
for img in doc.get("image_analysis", []):
rid = img.get("rid", "?")
img_type = img.get("type", "?")
lines.append(f"\n--- Image: {rid} (type={img_type}) ---")
lines.append(f" 描述: {img.get('description', '')[:300]}")
lt = img.get("logic_tree")
if lt:
lines.append(f" 逻辑树根节点: {lt.get('root', '?')}")
lines.append(" 节点详情:")
for node in lt.get("nodes", []):
nid = node.get("id", "?")
ntype = node.get("type", "?")
desc = node.get("description", "") or node.get("condition", "")
lines.append(f" [{ntype}] {nid}: {desc}")
branches = node.get("branches", [])
if branches:
for br in branches:
lines.append(f"{br['value']}{br['target']}")
# ---- Resolved Conflicts ----
conflicts = doc.get("resolved_conflicts", [])
if conflicts:
lines.append("\n\n=== RESOLVED_CONFLICTS (图文冲突仲裁) ===")
for c in conflicts:
lines.append(
f" [{c.get('conflict_type','?')}] {c.get('section','?')}: "
f"{c.get('source','?')}为准 — {c.get('correction','')}"
)
return "\n".join(lines)
def build_prompt(doc: dict) -> str:
"""Load the prompt template and inject the formatted document."""
template_path = Path(config.PROMPTS_DIR) / "step1_semantic_index.txt"
template = template_path.read_text(encoding="utf-8")
formatted_doc = format_document_for_prompt(doc)
prompt = template.replace("{document_json}", formatted_doc)
return prompt
def extract_json_from_response(text: str) -> str:
"""Robustly extract JSON from LLM response, handling markdown fences."""
# Try ```json ... ``` first
m = re.search(r"```(?:json)?\s*([\s\S]*?)```", text)
if m:
return m.group(1).strip()
# Try to find the outermost { ... }
start = text.find("{")
if start == -1:
raise ValueError("No JSON object found in LLM response")
depth = 0
for i in range(start, len(text)):
if text[i] == "{":
depth += 1
elif text[i] == "}":
depth -= 1
if depth == 0:
return text[start : i + 1]
raise ValueError("Unclosed JSON object in LLM response")
def call_llm(prompt: str, max_retries: int = 2) -> dict:
"""Send the prompt to the LLM and return the parsed semantic index JSON."""
client = config.llm_client()
for attempt in range(max_retries + 1):
print(f" LLM 调用 (尝试 {attempt + 1}/{max_retries + 1})...", flush=True)
try:
resp = client.chat.completions.create(
model=config.MODEL_NAME,
messages=[
{"role": "system", "content": "你是一个精确的 JSON 输出引擎。只输出合法的 JSON,不输出任何其他文字。"},
{"role": "user", "content": prompt},
],
temperature=config.TEMPERATURE,
max_tokens=config.MAX_TOKENS,
)
content = resp.choices[0].message.content
if content is None:
raise RuntimeError("LLM returned empty response")
json_str = extract_json_from_response(content)
result = json.loads(json_str)
# Quick structural validation
_validate_schema(result)
return result
except (json.JSONDecodeError, ValueError) as e:
print(f" JSON 解析失败: {e}")
if attempt < max_retries:
time.sleep(2)
else:
raise RuntimeError(f"无法从 LLM 响应中解析 JSON: {e}") from e
def _validate_schema(data: dict) -> None:
"""Validate the top-level schema of the semantic index."""
if "feature_name" not in data:
raise ValueError("semantic_index 缺少 'feature_name' 字段")
if "function_units" not in data:
raise ValueError("semantic_index 缺少 'function_units' 字段")
units = data["function_units"]
if not isinstance(units, list) or len(units) == 0:
raise ValueError("function_units 必须是非空数组")
for i, fu in enumerate(units):
if not fu.get("unit_id"):
raise ValueError(f"function_unit[{i}] 缺少 unit_id")
if not fu.get("name"):
raise ValueError(f"function_unit[{i}] 缺少 name")
def main():
print("=" * 60)
print("阶段一:宏观语义索引 (Semantic Index)")
print("=" * 60)
# 1. Load input
print(f"\n[1/4] 加载输入文档: {config.INPUT_JSON}")
doc = config.load_input_document()
print(f" 已加载 {len(doc.get('sections', []))} 个 section, "
f"{len(doc.get('image_analysis', []))} 张图片分析")
# 2. Build prompt
print(f"\n[2/4] 构造 Prompt...")
prompt = build_prompt(doc)
print(f" Prompt 长度: {len(prompt)} 字符 (~{len(prompt)//3} tokens)")
# 3. Call LLM
print(f"\n[3/4] 调用 LLM ({config.MODEL_NAME})...")
semantic_index = call_llm(prompt)
# 4. Save result
print(f"\n[4/4] 保存语义索引: {config.SEMANTIC_INDEX_JSON}")
config.save_json(semantic_index, config.SEMANTIC_INDEX_JSON)
n_concepts = len(semantic_index.get("concepts", []))
n_units = len(semantic_index.get("function_units", []))
print(f"\n完成! 提取了 {n_concepts} 个概念, {n_units} 个功能单元.")
print(f"输出: {config.SEMANTIC_INDEX_JSON}")
if __name__ == "__main__":
main()