117 lines
4.4 KiB
Python
117 lines
4.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
LLM-улучшатель документации Nubes Terraform Provider.
|
|
Прогоняет сгенерированные docs-generator'ом .md файлы через LLM,
|
|
чтобы сделать описания читаемыми и логичными.
|
|
|
|
Использование:
|
|
python3 05_generate_docs_llm.py generated/test/docs
|
|
"""
|
|
import json, os, sys, time
|
|
from pathlib import Path
|
|
from urllib.request import Request, urlopen
|
|
|
|
API_URL = "https://api.aillm.ru/v1/chat/completions"
|
|
API_KEY = "sk-ucI5YvOticoOQ9Kuj5K9mQ"
|
|
MODEL = "gpt-oss-120b"
|
|
|
|
SYSTEM_PROMPT = """Ты — технический писатель. Улучши ОДИН .md файл документации Terraform-провайдера: сделай описания грамотными и логичными.
|
|
|
|
ПРАВИЛА:
|
|
1. НЕ ВЫДУМЫВАЙ параметры, типы, значения. Только улучшай формулировки.
|
|
2. HTML-таблицы — меняй ТОЛЬКО текст внутри <td>...</td>. НЕ ломай теги.
|
|
3. HCL-блоки (```hcl ... ```) — НЕ ТРОГАТЬ вообще.
|
|
4. Navigation-строки — НЕ ТРОГАТЬ.
|
|
5. MAN-секцию с HTML — переведи в читаемый Markdown.
|
|
6. Верни ТОЛЬКО улучшенный Markdown. Без JSON, без пояснений, без ``` в начале/конце.
|
|
Просто готовый текст файла."""
|
|
|
|
def call_llm(prompt: str) -> str:
|
|
data = json.dumps({
|
|
"model": MODEL,
|
|
"messages": [
|
|
{"role": "system", "content": SYSTEM_PROMPT},
|
|
{"role": "user", "content": prompt},
|
|
],
|
|
"temperature": 0.15,
|
|
"max_tokens": 4096,
|
|
}).encode()
|
|
req = Request(API_URL, data=data, headers={
|
|
"Authorization": f"Bearer {API_KEY}",
|
|
"Content-Type": "application/json",
|
|
})
|
|
for attempt in range(3):
|
|
try:
|
|
with urlopen(req, timeout=120) as resp:
|
|
result = json.loads(resp.read())
|
|
content = result["choices"][0]["message"]["content"].strip()
|
|
# Strip markdown fences if present
|
|
if content.startswith("```"):
|
|
lines = content.split("\n")
|
|
if len(lines) > 2:
|
|
content = "\n".join(lines[1:-1])
|
|
return content
|
|
except Exception as e:
|
|
print(f" retry {attempt+1}/3: {e}", file=sys.stderr)
|
|
time.sleep(5)
|
|
raise RuntimeError("LLM failed after 3 retries")
|
|
|
|
def main():
|
|
docs_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("generated/test/docs")
|
|
if not docs_dir.exists():
|
|
print(f"ERROR: {docs_dir} not found", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# Process only main manual pages: Name.md (not _example, _params_*, _outputs, _ops)
|
|
all_md = sorted(docs_dir.glob("*.md"))
|
|
targets = []
|
|
for f in all_md:
|
|
name = f.stem
|
|
if name == "index":
|
|
continue
|
|
# Skip known non-manual suffixes
|
|
if any(name.endswith(sfx) for sfx in ["_example", "_params_create", "_params_modify", "_outputs", "_ops", "_params"]):
|
|
continue
|
|
# Also skip second-level (subresource) example files
|
|
if "_" in name:
|
|
# Could be subresource manual like "postgres_database"
|
|
# Check if there's a matching _example file
|
|
targets.append(f)
|
|
else:
|
|
targets.append(f)
|
|
|
|
print(f"Processing {len(targets)} manual pages...")
|
|
|
|
failed = []
|
|
for i, f in enumerate(targets):
|
|
svc = f.stem
|
|
print(f" [{i+1}/{len(targets)}] {svc}...", end=" ", flush=True)
|
|
|
|
content = f.read_text()
|
|
# Truncate very long files
|
|
if len(content) > 12000:
|
|
content = content[:12000] + "\n\n... (обрезано для LLM)\n"
|
|
|
|
prompt = f"Улучши этот файл документации:\n\n=== {f.name} ===\n{content}"
|
|
|
|
try:
|
|
improved = call_llm(prompt)
|
|
if improved and len(improved) > 100:
|
|
f.write_text(improved)
|
|
print("OK")
|
|
else:
|
|
print("SKIP (empty response)")
|
|
except Exception as e:
|
|
print(f"FAILED: {e}")
|
|
failed.append(svc)
|
|
|
|
time.sleep(1.5)
|
|
|
|
if failed:
|
|
print(f"\nFailed ({len(failed)}): {', '.join(failed)}")
|
|
else:
|
|
print(f"\nAll {len(targets)} OK")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|