Files
tf_provider/TOOLS/scripts/05_generate_docs_llm.py
T
2026-07-16 18:31:28 +04:00

135 lines
5.2 KiB
Python

#!/usr/bin/env python3
"""LLM-генератор документации для Nubes Terraform Provider.
Читает YAML-спеки, отправляет в LLM, сохраняет Markdown.
"""
import json, os, sys, time, yaml
from pathlib import Path
from urllib.request import Request, urlopen
from urllib.error import URLError
API_URL = "https://api.aillm.ru/v1/chat/completions"
API_KEY = "sk-ucI5YvOticoOQ9Kuj5K9mQ"
MODEL = "gpt-oss-120b"
PROMPT = """Ты — генератор документации Terraform-провайдера Nubes Cloud.
Отвечай ТОЛЬКО Markdown-кодом. Никаких пояснений до или после.
⛔ ЖЁСТКИЕ ПРАВИЛА (нарушать нельзя):
1. НЕ ВЫДУМЫВАЙ параметры. Только те, что есть в YAML.
Если у параметра нет description — напиши "—".
НИКОГДА не придумывай password, role, encoding и т.п.
2. Action-операции. ТОЛЬКО redeploy включается в документацию.
restart, recovery, reconcile — ИСКЛЮЧИТЬ.
3. Subresource-операции. Каждый subresource → отдельная секция.
Имя ресурса: nubes_{service}_{subresource}.
4. Lifecycle. suspend_on_destroy=true → "terraform destroy = Suspend".
adopt_existing_on_create → "terraform apply может подхватить существующий".
5. Outputs. Все поля из outputs.params. vault_secrets помечать как 🔒.
6. MAN. Если service_man есть — вставить как есть в секцию ## MAN.
7. ВЛОЖЕННЫЕ ПАРАМЕТРЫ (map-fixed/array-map-fixed).
Для каждого map-fixed параметра — показывать ВСЕ его sub_params как вложенную таблицу.
Для array-map-fixed — показывать структуру элемента.
Пример:
### clusterConfiguration (map-fixed)
| Параметр | Тип | Обязательный | По умолчанию | Описание |
|---|---|---|---|---|
| cpu | integer > 0 | да | 500 | Количество CPU в милликорах |
| memory | integer > 0 | да | 512 | Память в MB |
ФОРМАТ:
# Resource nubes_{name}
## MAN (если есть)
## Instance Operations (таблица)
## Create Parameters (таблица — для каждого map-fixed показывать sub_params)
## Modify Parameters (таблица)
## Subresources (таблица по каждому)
## Lifecycle
## Outputs (таблица)
YAML-спек:
```yaml
{yaml_content}
```"""
def call_llm(prompt_text: str) -> str:
data = json.dumps({
"model": MODEL,
"messages": [{"role": "user", "content": prompt_text}],
"temperature": 0.1,
"max_tokens": 8192,
}).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())
return result["choices"][0]["message"]["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 strip_service_man(yaml_text: str) -> str:
"""Убирает service_man (HTML-руководство) из YAML для сокращения токенов."""
lines = yaml_text.split('\n')
out = []
skip = False
for line in lines:
if line.strip().startswith('service_man:'):
out.append('service_man: "" # omitted for LLM token limit')
skip = True
continue
if skip:
# service_man может быть многострочным (HTML с отступами)
if line and line[0] not in (' ', '\t') and ':' in line:
skip = False
out.append(line)
continue
out.append(line)
return '\n'.join(out)
def main():
yaml_dir = sys.argv[1] if len(sys.argv) > 1 else "generated/test/resources_yaml"
docs_dir = sys.argv[2] if len(sys.argv) > 2 else "generated/test/docs_llm"
os.makedirs(docs_dir, exist_ok=True)
yamls = sorted(Path(yaml_dir).glob("*.yaml"))
print(f"Processing {len(yamls)} services...")
failed = []
for i, yf in enumerate(yamls):
svc_name = yf.stem.split("_", 1)[1] if "_" in yf.stem else yf.stem
print(f" [{i+1}/{len(yamls)}] {svc_name}...", end=" ", flush=True)
yaml_text = strip_service_man(yf.read_text())
prompt = PROMPT.replace("{yaml_content}", yaml_text)
try:
md = call_llm(prompt)
out = Path(docs_dir) / f"{yf.stem}.md"
out.write_text(md)
print("OK")
except Exception as e:
print(f"FAILED: {e}")
failed.append(svc_name)
time.sleep(2) # rate limit
if failed:
print(f"\nFailed ({len(failed)}): {', '.join(failed)}")
else:
print(f"\nAll {len(yamls)} OK")
if __name__ == "__main__":
main()