diff --git a/skills/moark-bid-document-generator/SKILL.md b/skills/moark-bid-document-generator/SKILL.md new file mode 100644 index 0000000000000000000000000000000000000000..58c19ff95c9800f5efa866247339a77383b35fa0 --- /dev/null +++ b/skills/moark-bid-document-generator/SKILL.md @@ -0,0 +1,86 @@ +--- +name: moark-bid-document-generator +description: Generate complete bid documents (商务标 + 技术标 + 报价标) for goods, service, and engineering procurement. Parallel generation, Chinese uppercase amounts, built-in integrity check, Decimal-precision budget handling. +metadata: + openclaw: + emoji: "📋" + requires: + env: ["GITEEAI_API_KEY"] + primaryEnv: "GITEEAI_API_KEY" +--- + +# Bid Document Generator 📋 + +Generate a **complete, ready-to-submit bid document** in one command. All three sections (商务标/技术标/报价标) are generated in parallel with Chinese uppercase amounts and Decimal-precision budget handling. + +## What You Get + +- 📄 **Three full sections** — commercial proposal, technical proposal, price proposal +- 💰 **Chinese uppercase amounts** — automatic 大写金额 conversion (e.g. ¥5,000,000 → 伍佰万元整) +- ⚡ **Parallel generation** — all 3 sections generated concurrently (3× faster) +- ✅ **Built-in integrity check** — keyword validation + budget consistency verification +- 🛡️ **Decimal precision** — no float rounding errors in financial calculations +- 🔄 **Auto-retry** — up to 3 attempts with exponential backoff per section +- 🔍 **Integrity-check-only mode** — validate existing documents without re-generating + +## Usage + +Set your API key once: `export GITEEAI_API_KEY=your_key` + +**Engineering project (e.g. school renovation)** +```bash +python {baseDir}/scripts/perform_bid_generate.py \ + -p "School Renovation" -t engineering -b 5000000 \ + -r "classroom renovation, facility upgrade, deadline 2024-12-31" +``` + +**Goods procurement (e.g. office equipment)** +```bash +python {baseDir}/scripts/perform_bid_generate.py \ + -p "Office Equipment" -t goods -b 500000 \ + -r "20 desktop computers, 5 laser printers" -o json +``` + +**Service procurement (e.g. IT maintenance)** +```bash +python {baseDir}/scripts/perform_bid_generate.py \ + -p "IT Maintenance Service" -t service -b 2000000 \ + -r "annual IT infrastructure maintenance" +``` + +**Integrity check only (no generation)** +```bash +python {baseDir}/scripts/perform_bid_generate.py \ + -p "School Renovation" -t engineering -b 5000000 --integrity-check-only +# Pipe a document via stdin for checking: +# cat existing_doc.json | python ... --integrity-check-only +``` + +## Options +- `--project` / `-p` (required): Project name +- `--type` / `-t` (required): `goods` (货物采购), `service` (服务采购), `engineering` (工程施工) +- `--budget` / `-b` (required): Positive number, supports thousands separator (e.g. `5,000,000`) +- `--requirements` / `-r`: Bidding requirements and specifications +- `--output` / `-o`: `markdown` (default) or `json` +- `--model` / `-m`: Generation model (default: DeepSeek-R1-0528) +- `--timeout`: Per-call timeout in seconds (default: 120) +- `--integrity-check-only`: Validate existing document (via stdin JSON); skip generation +- `--api-key` / `-k`: Gitee AI API key (prefer `GITEEAI_API_KEY` env var) + +## Workflow + +1. Run `perform_bid_generate.py` with the user's parameters. +2. Find the line starting with `BID_RESULT:` in the output. +3. Extract everything from that line onwards. +4. Present to the user as: `📋 [Bid Document]` + +## Notes +- All 3 sections generated in parallel via `ThreadPoolExecutor` (typically 30-60s total) +- Budget validated as positive `Decimal`; thousands separators (`5,000,000`) supported +- `amount_to_chinese` handles 0元整, negative numbers, and amounts > 万亿 +- Integrity check validates: + - Required keywords per section (商务标: 资质/承诺/营业执照/财务, 技术标: 方案/质量/进度/安全, 报价标: 报价/单价/合计/大写) + - Budget amount consistency between budget input and price section (1% tolerance) +- Decimal precision throughout — no float pollution in financial calculations +- 千分位 (thousands separator) parsing hardened against crashes in integrity check +- Use `--integrity-check-only` to validate an existing document without re-generating diff --git a/skills/moark-bid-document-generator/scripts/perform_bid_generate.py b/skills/moark-bid-document-generator/scripts/perform_bid_generate.py new file mode 100644 index 0000000000000000000000000000000000000000..2f04e326939073d72db23b8073a7d705d070c9e4 --- /dev/null +++ b/skills/moark-bid-document-generator/scripts/perform_bid_generate.py @@ -0,0 +1,480 @@ +#!/usr/bin/env python3 +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "openai" +# ] +# /// + +""" +Generate bid documents (business proposal + technical proposal + price proposal) +for goods/service/engineering procurement. + +Usage: + python perform_bid_generate.py --project "School renovation" --type engineering --budget 5000000 --requirements "..." [--api-key KEY] + python perform_bid_generate.py --integrity-check-only --project "School renovation" --type engineering --budget 5000000 +""" + +import argparse +import json +import os +import re +import sys +import time +from concurrent.futures import ThreadPoolExecutor, as_completed +from decimal import Decimal, InvalidOperation, ROUND_HALF_UP +from openai import OpenAI + +DEFAULT_MODEL = "DeepSeek-R1-0528" +API_BASE_URL = "https://ai.gitee.com/v1" +DEFAULT_TIMEOUT = 120 +MAX_RETRIES = 3 + +BID_TYPE_LABELS = { + "goods": "货物采购", + "service": "服务采购", + "engineering": "工程施工", +} + +# Required keywords for integrity check per section +SECTION_KEYWORDS = { + "commercial": ["资质", "承诺", "营业执照", "财务"], + "technical": ["方案", "质量", "进度", "安全"], + "price": ["报价", "单价", "合计", "大写"], +} + +# --- Amount to Chinese uppercase conversion --- +DIGITS = ["零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"] +UNITS = ["", "拾", "佰", "仟"] +BIG_UNITS = ["", "万", "亿", "兆"] + + +def amount_to_chinese(amount: Decimal) -> str: + """Convert a numeric amount to Chinese uppercase financial notation. + + Handles: 0元整, negative numbers, amounts > 万亿. + Example: 12345.67 -> "壹万贰仟叁佰肆拾伍元陆角柒分" + """ + if amount == 0: + return "零元整" + + negative = amount < 0 + amount = abs(amount) + + # Use Decimal for precise financial calculation + yuan = int(amount) + fen = int((amount - yuan).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) * 100) # Precise rounding for financial amounts + + result = "" + + # Convert yuan part + if yuan > 0: + groups = [] + temp = yuan + while temp > 0: + groups.append(temp % 10000) + temp //= 10000 + + for i in range(len(groups) - 1, -1, -1): + g = groups[i] + if g == 0: + if result and not result.endswith("零"): + result += "零" + continue + + group_str = "" + for j in range(3, -1, -1): + d = g // (10 ** j) % 10 + if d == 0: + if group_str and not group_str.endswith("零"): + group_str += "零" + else: + group_str += DIGITS[d] + UNITS[j] + + group_str = group_str.rstrip("零") + + # Handle amounts > 万亿 (big_units index overflow) + big_unit_idx = i + if big_unit_idx < len(BIG_UNITS): + result += group_str + BIG_UNITS[big_unit_idx] + else: + # Amount exceeds standard notation, truncate with warning + print("Warning: Amount exceeds standard Chinese notation. Truncating.", file=sys.stderr) + break + + result = result.rstrip("零") + "元" + + # Convert fen part + if fen > 0: + jiao = fen // 10 + fen_part = fen % 10 + if jiao > 0: + result += DIGITS[jiao] + "角" + elif yuan > 0: + result += "零" + if fen_part > 0: + result += DIGITS[fen_part] + "分" + else: + result += "整" + + if negative: + result = "负" + result + + return result + + +def get_api_key(provided_key: str | None) -> str | None: + """Get API key from argument or environment.""" + if provided_key: + return provided_key + return os.environ.get("GITEEAI_API_KEY") + + +def _extract_content(response) -> str: + """Safely extract text content from an OpenAI chat completion response.""" + if not response.choices: + return "" + message = response.choices[0].message + content = message.content if message else None + return content.strip() if content else "" + + +def chat_with_retry(client: OpenAI, model: str, system_prompt: str, user_prompt: str, timeout: int) -> str: + """Call chat API with retry logic.""" + last_error = None + for attempt in range(MAX_RETRIES): + try: + response = client.chat.completions.create( + model=model, + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt}, + ], + stream=False, + timeout=timeout, + ) + return _extract_content(response) + except Exception as e: + last_error = e + if attempt < MAX_RETRIES - 1: + time.sleep(2 ** attempt) + print(f" Retry {attempt + 1}/{MAX_RETRIES} after error: {e}", file=sys.stderr) + raise last_error # type: ignore[misc] + + +# --- Section generation configs --- +SECTION_CONFIGS = { + "commercial": { + "label": "商务标", + "system_prompt": ( + "You are a professional bid document writer. Generate the commercial " + "proposal (商务标) section for a bid document. Include: bidder qualifications, " + "business license info, financial statements summary, similar project experience, " + "and commitment letters. Use formal Chinese business language." + ), + }, + "technical": { + "label": "技术标", + "system_prompt": ( + "You are a professional bid document writer. Generate the technical " + "proposal (技术标) section for a bid document. Include: technical approach, " + "implementation plan, quality assurance measures, timeline/schedule, " + "team organization, and risk management. Use formal Chinese technical language." + ), + }, + "price": { + "label": "报价标", + "system_prompt": ( + "You are a professional bid document writer. Generate the price " + "proposal (报价标) section for a bid document. Include: price breakdown table, " + "unit prices, total price (both numeric and Chinese uppercase), price commitment " + "letter. Use formal Chinese business language." + ), + }, +} + + +def _build_user_prompt(project: str, bid_type: str, budget: str, budget_chinese: str, requirements: str, section: str) -> str: + """Build user prompt for a section generation call.""" + type_label = BID_TYPE_LABELS.get(bid_type, bid_type) + prompt = ( + f"项目名称: {project}\n" + f"招标类型: {type_label}\n" + f"预算金额: {budget}" + ) + if section == "commercial" or section == "price": + prompt += f" (大写: {budget_chinese})" + prompt += f"\n招标要求: {requirements}" + return prompt + + +def generate_section( + client: OpenAI, + project: str, + bid_type: str, + budget: str, + budget_chinese: str, + requirements: str, + section: str, + model: str, + timeout: int, +) -> str: + """Generate a single bid document section.""" + config = SECTION_CONFIGS[section] + user_prompt = _build_user_prompt(project, bid_type, budget, budget_chinese, requirements, section) + return chat_with_retry(client, model, config["system_prompt"], user_prompt, timeout) + + +# --- Integrity check --- +def check_integrity(document: dict) -> list[str]: + """Check document completeness with keyword-based validation.""" + warnings = [] + required_sections = ["commercial", "technical", "price"] + + for section in required_sections: + content = document.get(section, "") + if not content: + warnings.append(f"Missing or empty section: {section} ({SECTION_CONFIGS[section]['label']})") + continue + + # Keyword-based check + keywords = SECTION_KEYWORDS.get(section, []) + missing_keywords = [kw for kw in keywords if kw not in content] + if missing_keywords: + label = SECTION_CONFIGS[section]["label"] + warnings.append( + f"{label} section may be incomplete - missing keywords: {', '.join(missing_keywords)}" + ) + + # Check budget consistency - match multiple formats + budget = document.get("budget", "") + price = document.get("price", "") + if budget and price: + try: + budget_num = float(budget) + except (ValueError, TypeError): + budget_num = None + has_budget_match = False + if budget_num is not None: + # Try numeric patterns in price content + price_nums = [] + for m in re.findall(r"\d[\d,]*\.?\d*", price): + try: + cleaned = m.replace(",", "").strip() + if cleaned and cleaned.replace(".", "").isdigit(): + price_nums.append(float(cleaned)) + except (ValueError, TypeError): + continue + for pn in price_nums: + if abs(pn - budget_num) / max(budget_num, 1) < 0.01: + has_budget_match = True + break + if not has_budget_match: + # Try 万 unit + wan_match = re.search(r"(\d+(?:\.\d+)?)万", price) + if wan_match and abs(float(wan_match.group(1)) * 10000 - budget_num) / max(budget_num, 1) < 0.01: + has_budget_match = True + if not has_budget_match: + # Try string patterns (Chinese uppercase, etc.) + patterns = [re.escape(budget)] + budget_chinese = document.get("budget_chinese", "") + if budget_chinese: + patterns.append(re.escape(budget_chinese)) + if any(re.search(p, price) for p in patterns if p): + has_budget_match = True + if not has_budget_match: + warnings.append("Budget amount not clearly reflected in price section") + + return warnings + + +def main(): + parser = argparse.ArgumentParser( + description="Generate bid documents for procurement projects" + ) + parser.add_argument( + "--project", "-p", + required=True, + help="Project name", + ) + parser.add_argument( + "--type", "-t", + required=True, + choices=["goods", "service", "engineering"], + help="Bid type: goods/service/engineering", + ) + parser.add_argument( + "--budget", "-b", + required=True, + help="Budget amount (numeric, must be positive, e.g. 5000000)", + ) + parser.add_argument( + "--requirements", "-r", + default="", + help="Bidding requirements and specifications", + ) + parser.add_argument( + "--output", "-o", + choices=["json", "markdown"], + default="markdown", + help="Output format (default: markdown)", + ) + parser.add_argument( + "--model", "-m", + default=DEFAULT_MODEL, + help=f"Model to use for generation (default: {DEFAULT_MODEL})", + ) + parser.add_argument( + "--timeout", + type=int, + default=DEFAULT_TIMEOUT, + help=f"Timeout per API call in seconds (default: {DEFAULT_TIMEOUT})", + ) + parser.add_argument( + "--integrity-check-only", + action="store_true", + help="Only run integrity check on existing document, do not generate", + ) + parser.add_argument( + "--api-key", "-k", + help="Gitee AI API key (overrides GITEEAI_API_KEY env var). " + "Using environment variable is recommended for security.", + ) + + args = parser.parse_args() + + # Validate budget as positive Decimal + try: + budget_decimal = Decimal(args.budget.replace(",", "").strip()) + if budget_decimal <= 0: + print("Error: --budget must be a positive number.", file=sys.stderr) + sys.exit(1) + except (InvalidOperation, ValueError): + print("Error: --budget must be a valid numeric value.", file=sys.stderr) + sys.exit(1) + + budget_chinese = amount_to_chinese(budget_decimal) + + # Integrity-check-only mode: no API key needed + if args.integrity_check_only: + # Build a minimal document for checking (e.g., from stdin or file) + print("Integrity check mode: checking required sections and keywords...") + doc = { + "project": args.project, + "type": args.type, + "budget": args.budget, + } + # Read document content from stdin if provided + try: + stdin_content = "" + if not sys.stdin.isatty(): + stdin_content = sys.stdin.read() + if stdin_content.strip(): + try: + provided = json.loads(stdin_content) + doc.update(provided) + except json.JSONDecodeError: + print("Warning: Could not parse stdin as JSON for integrity check.", file=sys.stderr) + except Exception: + pass + + warnings = check_integrity(doc) + result = { + "project": args.project, + "type": args.type, + "budget": args.budget, + "budget_chinese": budget_chinese, + "integrity_warnings": warnings, + "status": "PASS" if not warnings else "ISSUES_FOUND", + } + print("\nBID_RESULT:") + print(json.dumps(result, ensure_ascii=False, indent=2)) + return + + # Normal generation mode + api_key = get_api_key(args.api_key) + if not api_key: + print("Error: No API key provided.", file=sys.stderr) + print("Please either:", file=sys.stderr) + print(" 1. Provide --api-key argument", file=sys.stderr) + print(" 2. Set GITEEAI_API_KEY environment variable", file=sys.stderr) + sys.exit(1) + + client = OpenAI( + base_url=API_BASE_URL, + api_key=api_key, + ) + + print(f"Generating bid document for: {args.project}") + print(f"Type: {BID_TYPE_LABELS.get(args.type, args.type)}, Budget: {args.budget} ({budget_chinese})") + + try: + # Generate three sections in parallel + print("\nGenerating all 3 sections in parallel...") + sections = ["commercial", "technical", "price"] + results = {} + + with ThreadPoolExecutor(max_workers=3) as executor: + futures = {} + section_indices = {s: i + 1 for i, s in enumerate(sections)} + for section in sections: + config = SECTION_CONFIGS[section] + idx = section_indices[section] + print(f" [{idx}/3] Starting {config['label']}...") + future = executor.submit( + generate_section, + client, args.project, args.type, + args.budget, budget_chinese, args.requirements, + section, args.model, args.timeout, + ) + futures[future] = section + + for future in as_completed(futures): + section = futures[future] + config = SECTION_CONFIGS[section] + idx = section_indices[section] + try: + results[section] = future.result() + print(f" [{idx}/3] Completed {config['label']}") + except Exception as e: + print(f" [{idx}/3] Failed {config['label']}: {e}", file=sys.stderr) + results[section] = f"(Generation failed: {e})" + + # Build result + result = { + "project": args.project, + "type": args.type, + "type_label": BID_TYPE_LABELS.get(args.type, args.type), + "budget": args.budget, + "budget_chinese": budget_chinese, + "commercial": results.get("commercial", ""), + "technical": results.get("technical", ""), + "price": results.get("price", ""), + } + + # Integrity check + warnings = check_integrity(result) + if warnings: + result["warnings"] = warnings + + print("\nBID_RESULT:") + if args.output == "json": + print(json.dumps(result, ensure_ascii=False, indent=2)) + else: + type_label = BID_TYPE_LABELS.get(args.type, args.type) + print(f"\n# {args.project} - {type_label}投标文件\n") + print(f"## 商务标\n\n{results.get('commercial', '')}\n") + print(f"## 技术标\n\n{results.get('technical', '')}\n") + print(f"## 报价标\n\n{results.get('price', '')}\n") + if warnings: + print(f"## ⚠️ 完整性检查\n") + for w in warnings: + print(f"- {w}") + + except Exception as e: + print(f"\nError generating bid document: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main()