init the project
This commit is contained in:
+200
@@ -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()
|
||||
Reference in New Issue
Block a user