feat: add D.intelligence Agent Corps (9 skills + shared infra)

Add 9 agent skills (#70-#77, #88) for D.intelligence business operations:
brand guardian, brand editor, doc secretary, quotation manager, service
architect, marketing manager, back office manager, account manager, and
skill update meta-agent. Includes shared Python package (dintel), reference
docs, document/quotation templates, service module CSVs, cross-device
installer, and comprehensive user guide.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 00:49:04 +09:00
parent 72a6be6a74
commit 338176abbe
71 changed files with 15054 additions and 2 deletions

View File

@@ -0,0 +1,65 @@
# D.intelligence Quotation Manager
> **Agent #73** | `dintel-quotation-mgr` v1.0.0 | D.intelligence Agent Corps
Generate professional quotations and estimates for D.intelligence service modules using a multi-agent sub-system (Scope, Resource, Pricing, Output).
## Agent Corps Context
- **Agent #73** -- Quotation Manager (Multi-Agent)
- **Collaborates with**: Agent #70 (Brand Guardian), Agent #71 (Brand Editor)
- **Shared constants**: `dintel-shared/src/dintel/brand.py` (colors, terminology, style tokens)
- **Excel utilities**: `dintel-shared/src/dintel/excel.py` (branded workbook generation)
## Universal Guardrails
1. **Never send to clients without Andrew's approval** -- All quotations require Andrew's review.
2. **Never delete -- always archive** -- Move outdated quotes to archive; never permanently delete.
3. **Never commit pricing without Andrew's sign-off** -- CRITICAL. All pricing is draft until approved.
4. **Korean-first, bilingual notation** -- Korean primary; jargon uses 한글(English) notation on first use.
5. **Never cross-reference client data without consent** -- Client data is siloed by account.
## Autonomy Level: Draft & Wait
This agent generates quotation drafts and STOPS. It never finalizes or sends quotes without Andrew's explicit approval. All generated files are marked as DRAFT with yellow-highlighted price cells.
## Workflow
1. Receive client brief
2. Run Scope Agent -> map requirements to modules (A1--G4)
3. Run Resource Agent -> estimate hours, timeline, team
4. Run Pricing Agent -> calculate prices, apply discounts
5. Run Output Generator -> produce branded .xlsx
6. **STOP** -- Notify Andrew for review
7. Process feedback from Google Sheets comments -> update `shared/feedback-log.md`
8. Revise if requested, then await final approval
## Quick Reference
- **Full skill definition**: `../desktop/SKILL.md` (sub-agents, pricing tables, discount rules, output format)
- **Pricing reference**: `../shared/pricing-reference.md` and `../../dintel-shared/references/pricing-reference.md`
- **Feedback log**: `../shared/feedback-log.md`
- **Generate script**: `scripts/generate_quotation.py`
- **Excel utilities**: `../../dintel-shared/src/dintel/excel.py`
## Quotation Template Library
Reference templates are available in `shared/quotation-templates/`:
| Template | Filename | Description |
|----------|----------|-------------|
| Standard 2026 | `D.intelligence-표준 견적서_2026.xlsx` | Current standard quotation template |
| GA Analytics | `[템플릿] D.intelligence-Google Analytics-표준 견적서.xlsx` | GA4/GTM service quotes |
| Content Marketing | `[템플릿] D.intelligence-콘텐츠 마케팅-표준 견적서 2026.xlsx` | Content marketing quotes |
| GA Training | `[템플릿] D.intelligence-디지털 마케터를 위한 GA활용-중급-견적 _ 커리큘럼.xlsx` | Training/workshop quotes |
Use these as structural references for sheet layout, column headers, and formatting conventions when generating new quotations.
## Key Rules
- Quotation reference format: `DI-Q-{YYYYMMDD}-{NNN}`
- All prices are VAT 별도
- Discounts: apply highest single base discount; 재계약 10% stacks on top; never exceed 35% total
- Payment terms default: 착수금 50% / 완료 후 50%
- Validity: 견적 유효기간 30일
- File naming: `DI-Q-{YYYYMMDD}-{NNN}_{ClientName}_DRAFT.xlsx`

View File

@@ -0,0 +1,355 @@
"""
D.intelligence Quotation Generator
Agent #73 - dintel-quotation-mgr
Generates branded Excel .xlsx quotation files using dintel-shared utilities.
This is a stub script -- extend with full implementation as needed.
Usage:
python generate_quotation.py --client "고객사명" --modules A3,T6 --output ./output/
Dependencies:
- dintel-shared (../../dintel-shared/)
- openpyxl
"""
from __future__ import annotations
import argparse
import sys
from dataclasses import dataclass, field
from datetime import date, timedelta
from pathlib import Path
from typing import Optional
# ---------------------------------------------------------------------------
# Pricing data (mirrors shared/pricing-reference.md)
# ---------------------------------------------------------------------------
MODULE_PRICING: dict[str, dict] = {
# Analysis (진단)
"A1": {"name": "비즈니스·브랜드 진단", "duration": "2-3주", "min": 3_000_000, "max": 5_000_000, "phase": "Analysis"},
"A2": {"name": "고객·소비자 분석", "duration": "3-4주", "min": 4_000_000, "max": 7_000_000, "phase": "Analysis"},
"A3": {"name": "데이터 분석 (웹·앱)", "duration": "3-5주", "min": 4_000_000, "max": 8_000_000, "phase": "Analysis"},
"A4": {"name": "디지털 마케팅 진단", "duration": "2-4주", "min": 3_000_000, "max": 6_000_000, "phase": "Analysis"},
"A5": {"name": "퍼포먼스 마케팅 진단", "duration": "2-3주", "min": 3_000_000, "max": 5_000_000, "phase": "Analysis"},
"A6": {"name": "운영·관리 진단", "duration": "2-3주", "min": 2_000_000, "max": 4_000_000, "phase": "Analysis"},
# Treatment (처방)
"T1": {"name": "브랜드 스토리텔링 & 가이드", "duration": "4-8주", "min": 5_000_000, "max": 12_000_000, "phase": "Treatment"},
"T2": {"name": "고객 접점 경험 최적화", "duration": "4-6주", "min": 4_000_000, "max": 8_000_000, "phase": "Treatment"},
"T3": {"name": "디지털 자산 통합관리", "duration": "4-8주", "min": 6_000_000, "max": 15_000_000, "phase": "Treatment"},
"T4": {"name": "콘텐츠 마케팅", "duration": "4-8주", "min": 4_000_000, "max": 10_000_000, "phase": "Treatment"},
"T5": {"name": "광고·전환 최적화", "duration": "3-6주", "min": 4_000_000, "max": 8_000_000, "phase": "Treatment"},
"T6": {"name": "Brand Visibility Treatment", "duration": "4-12주", "min": 5_000_000, "max": 15_000_000, "phase": "Treatment"},
"T7": {"name": "운영 시스템·자동화", "duration": "4-8주", "min": 4_000_000, "max": 10_000_000, "phase": "Treatment"},
# Growth (성장)
"G1": {"name": "퍼포먼스 마케팅", "duration": "월간", "min": 2_000_000, "max": 5_000_000, "phase": "Growth", "monthly": True},
"G2": {"name": "콘텐츠 마케팅 대행", "duration": "월간", "min": 3_000_000, "max": 6_000_000, "phase": "Growth", "monthly": True},
"G3": {"name": "모니터링·이슈관리", "duration": "월간", "min": 2_000_000, "max": 4_000_000, "phase": "Growth", "monthly": True},
"G4": {"name": "연간 계약·운영", "duration": "12개월", "min": 0, "max": 0, "phase": "Growth", "monthly": True},
}
COMPLEXITY_PERCENTILE = {
"standard": 0.30,
"complex": 0.60,
"enterprise": 0.90,
}
DISCOUNT_POLICIES = {
"multi_3plus": {"label": "3개 모듈 이상 동시 계약", "rate": 0.15},
"analysis_treatment": {"label": "Analysis → Treatment 연계", "rate": 0.20},
"full_cycle": {"label": "Full cycle (A→T→G)", "rate": 0.25},
"g4_annual": {"label": "G4 연간 계약", "rate": 0.20},
"renewal": {"label": "재계약 (기존 고객)", "rate": 0.10, "stackable": True},
}
# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------
@dataclass
class LineItem:
code: str
name: str
phase: str
complexity: str = "standard"
base_price: int = 0
months: int = 1 # for Growth modules
subtotal: int = 0
@dataclass
class QuotationDraft:
ref: str = ""
client_name: str = ""
industry: str = ""
date_created: date = field(default_factory=date.today)
validity_days: int = 30
line_items: list[LineItem] = field(default_factory=list)
subtotal: int = 0
discount_label: str = ""
discount_rate: float = 0.0
discount_amount: int = 0
total_before_vat: int = 0
is_renewal: bool = False
# ---------------------------------------------------------------------------
# Pricing logic
# ---------------------------------------------------------------------------
def calculate_price(code: str, complexity: str = "standard") -> int:
"""Calculate module price based on complexity percentile within range."""
module = MODULE_PRICING.get(code)
if not module:
raise ValueError(f"Unknown module code: {code}")
pct = COMPLEXITY_PERCENTILE.get(complexity, 0.30)
price_range = module["max"] - module["min"]
return int(module["min"] + price_range * pct)
def determine_discount(line_items: list[LineItem], is_renewal: bool = False) -> tuple[str, float]:
"""Determine the highest applicable base discount, plus renewal if applicable."""
phases = {item.phase for item in line_items}
num_modules = len(line_items)
has_g4 = any(item.code == "G4" for item in line_items)
# Determine base discount (highest wins)
base_label = ""
base_rate = 0.0
if phases >= {"Analysis", "Treatment", "Growth"}:
base_label = DISCOUNT_POLICIES["full_cycle"]["label"]
base_rate = DISCOUNT_POLICIES["full_cycle"]["rate"]
elif "Analysis" in phases and "Treatment" in phases:
base_label = DISCOUNT_POLICIES["analysis_treatment"]["label"]
base_rate = DISCOUNT_POLICIES["analysis_treatment"]["rate"]
elif has_g4:
base_label = DISCOUNT_POLICIES["g4_annual"]["label"]
base_rate = DISCOUNT_POLICIES["g4_annual"]["rate"]
elif num_modules >= 3:
base_label = DISCOUNT_POLICIES["multi_3plus"]["label"]
base_rate = DISCOUNT_POLICIES["multi_3plus"]["rate"]
# Stack renewal discount
if is_renewal and base_rate > 0:
base_label += " + 재계약"
base_rate = min(base_rate + 0.10, 0.35)
elif is_renewal:
base_label = DISCOUNT_POLICIES["renewal"]["label"]
base_rate = 0.10
return base_label, base_rate
def build_quotation(
client_name: str,
modules: list[tuple[str, str]], # [(code, complexity), ...]
industry: str = "",
is_renewal: bool = False,
growth_months: int = 3,
) -> QuotationDraft:
"""Build a complete quotation draft."""
today = date.today()
ref = f"DI-Q-{today.strftime('%Y%m%d')}-001"
draft = QuotationDraft(
ref=ref,
client_name=client_name,
industry=industry,
date_created=today,
is_renewal=is_renewal,
)
for code, complexity in modules:
module = MODULE_PRICING[code]
price = calculate_price(code, complexity)
months = growth_months if module.get("monthly") else 1
item = LineItem(
code=code,
name=module["name"],
phase=module["phase"],
complexity=complexity,
base_price=price,
months=months,
subtotal=price * months,
)
draft.line_items.append(item)
draft.subtotal = sum(item.subtotal for item in draft.line_items)
draft.discount_label, draft.discount_rate = determine_discount(draft.line_items, is_renewal)
draft.discount_amount = int(draft.subtotal * draft.discount_rate)
draft.total_before_vat = draft.subtotal - draft.discount_amount
return draft
# ---------------------------------------------------------------------------
# Excel generation (stub -- requires openpyxl and dintel-shared)
# ---------------------------------------------------------------------------
def generate_xlsx(draft: QuotationDraft, output_dir: Path) -> Path:
"""Generate branded .xlsx quotation file.
TODO: Implement full branded workbook using dintel-shared/src/dintel/excel.py.
This stub creates a basic workbook structure.
"""
try:
from openpyxl import Workbook
from openpyxl.styles import Alignment, Font, PatternFill
except ImportError:
print("ERROR: openpyxl is required. Install with: pip install openpyxl", file=sys.stderr)
sys.exit(1)
wb = Workbook()
# -- Sheet 1: Cover --
ws_cover = wb.active
ws_cover.title = "표지"
ws_cover["B2"] = "D.intelligence :: SMART Marketing Clinic ::"
ws_cover["B2"].font = Font(name="Pretendard", size=16, bold=True)
ws_cover["B4"] = "견적서 (Quotation)"
ws_cover["B4"].font = Font(name="Pretendard", size=24, bold=True)
ws_cover["B6"] = f"고객사: {draft.client_name}"
ws_cover["B7"] = f"업종: {draft.industry}"
ws_cover["B8"] = f"견적번호: {draft.ref}"
ws_cover["B9"] = f"작성일: {draft.date_created.isoformat()}"
ws_cover["B10"] = f"유효기간: {(draft.date_created + timedelta(days=draft.validity_days)).isoformat()}"
ws_cover["B12"] = "DRAFT -- 검토 대기"
ws_cover["B12"].font = Font(color="FF0000", bold=True, size=14)
# -- Sheet 2: Scope --
ws_scope = wb.create_sheet("서비스 범위")
headers = ["모듈 코드", "모듈명", "Phase", "복잡도", "비고"]
for col, header in enumerate(headers, 1):
cell = ws_scope.cell(row=1, column=col, value=header)
cell.font = Font(bold=True)
for row, item in enumerate(draft.line_items, 2):
ws_scope.cell(row=row, column=1, value=item.code)
ws_scope.cell(row=row, column=2, value=item.name)
ws_scope.cell(row=row, column=3, value=item.phase)
ws_scope.cell(row=row, column=4, value=item.complexity)
# -- Sheet 3: Timeline (placeholder) --
ws_timeline = wb.create_sheet("일정")
ws_timeline["A1"] = "Phase"
ws_timeline["B1"] = "Module"
ws_timeline["C1"] = "Duration"
ws_timeline["A1"].font = Font(bold=True)
ws_timeline["B1"].font = Font(bold=True)
ws_timeline["C1"].font = Font(bold=True)
for row, item in enumerate(draft.line_items, 2):
ws_timeline.cell(row=row, column=1, value=item.phase)
ws_timeline.cell(row=row, column=2, value=f"{item.code} {item.name}")
module = MODULE_PRICING[item.code]
ws_timeline.cell(row=row, column=3, value=module["duration"])
# -- Sheet 4: Pricing --
ws_pricing = wb.create_sheet("견적 내역")
draft_fill = PatternFill(start_color="FFFF00", end_color="FFFF00", fill_type="solid")
price_headers = ["모듈 코드", "모듈명", "단가 (원)", "수량/개월", "소계 (원)"]
for col, header in enumerate(price_headers, 1):
cell = ws_pricing.cell(row=1, column=col, value=header)
cell.font = Font(bold=True)
for row, item in enumerate(draft.line_items, 2):
ws_pricing.cell(row=row, column=1, value=item.code)
ws_pricing.cell(row=row, column=2, value=item.name)
price_cell = ws_pricing.cell(row=row, column=3, value=item.base_price)
price_cell.fill = draft_fill
price_cell.number_format = "#,##0"
ws_pricing.cell(row=row, column=4, value=item.months)
subtotal_cell = ws_pricing.cell(row=row, column=5, value=item.subtotal)
subtotal_cell.fill = draft_fill
subtotal_cell.number_format = "#,##0"
summary_row = len(draft.line_items) + 3
ws_pricing.cell(row=summary_row, column=4, value="소계").font = Font(bold=True)
ws_pricing.cell(row=summary_row, column=5, value=draft.subtotal).number_format = "#,##0"
if draft.discount_rate > 0:
summary_row += 1
ws_pricing.cell(row=summary_row, column=3, value=draft.discount_label)
ws_pricing.cell(row=summary_row, column=4, value=f"-{int(draft.discount_rate * 100)}%")
disc_cell = ws_pricing.cell(row=summary_row, column=5, value=-draft.discount_amount)
disc_cell.number_format = "#,##0"
disc_cell.font = Font(color="FF0000")
summary_row += 1
ws_pricing.cell(row=summary_row, column=4, value="합계 (VAT 별도)").font = Font(bold=True, size=12)
total_cell = ws_pricing.cell(row=summary_row, column=5, value=draft.total_before_vat)
total_cell.font = Font(bold=True, size=12)
total_cell.number_format = "#,##0"
total_cell.fill = draft_fill
summary_row += 1
ws_pricing.cell(row=summary_row, column=5, value="Andrew 검토 필요").font = Font(color="FF0000", italic=True)
# -- Sheet 5: Terms --
ws_terms = wb.create_sheet("계약 조건")
terms = [
("결제 조건", "착수금 50% / 완료 후 50%"),
("견적 유효기간", "발행일로부터 30일"),
("부가세", "별도 (10%)"),
("범위 변경", "서면 합의 후 별도 견적"),
("계약 해지", "착수 전 전액 환불 / 착수 후 진행분 정산"),
("", ""),
("D.intelligence", "SMART Marketing Clinic"),
("Website", "dintelligence.co.kr"),
("담당자", "Andrew Yim"),
]
for row, (label, value) in enumerate(terms, 1):
ws_terms.cell(row=row, column=1, value=label).font = Font(bold=True)
ws_terms.cell(row=row, column=2, value=value)
# Save
output_dir.mkdir(parents=True, exist_ok=True)
filename = f"{draft.ref}_{draft.client_name.replace(' ', '_')}_DRAFT.xlsx"
filepath = output_dir / filename
wb.save(filepath)
return filepath
# ---------------------------------------------------------------------------
# CLI entry point
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(description="D.intelligence Quotation Generator")
parser.add_argument("--client", required=True, help="Client name (고객사명)")
parser.add_argument("--industry", default="", help="Client industry (업종)")
parser.add_argument("--modules", required=True, help="Comma-separated module codes (e.g., A3,T6,G2)")
parser.add_argument("--complexity", default="standard", choices=["standard", "complex", "enterprise"],
help="Default complexity tier for all modules")
parser.add_argument("--renewal", action="store_true", help="Existing client (재계약)")
parser.add_argument("--growth-months", type=int, default=3, help="Number of months for Growth modules")
parser.add_argument("--output", default="./output", help="Output directory")
args = parser.parse_args()
module_list = [(code.strip(), args.complexity) for code in args.modules.split(",")]
draft = build_quotation(
client_name=args.client,
modules=module_list,
industry=args.industry,
is_renewal=args.renewal,
growth_months=args.growth_months,
)
filepath = generate_xlsx(draft, Path(args.output))
print(f"Quotation draft generated: {filepath}")
print(f" Reference: {draft.ref}")
print(f" Client: {draft.client_name}")
print(f" Modules: {', '.join(item.code for item in draft.line_items)}")
print(f" Subtotal: {draft.subtotal:,}")
if draft.discount_rate > 0:
print(f" Discount: {draft.discount_label} (-{int(draft.discount_rate * 100)}%, -{draft.discount_amount:,}원)")
print(f" Total (VAT 별도): {draft.total_before_vat:,}")
print()
print("STATUS: DRAFT -- Andrew 검토 대기")
if __name__ == "__main__":
main()