#!/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()