n8n-workflow-evaluator / custom_criteria_generator.py
suwankim
Deploy n8n workflow evaluator
77329f6
"""
์ปค์Šคํ…€ ์‹ฌ์‚ฌ ๊ธฐ์ค€ ์ƒ์„ฑ ๋ชจ๋“ˆ
์‚ฌ์šฉ์ž ์ž…๋ ฅ ๊ธฐ์ค€์„ ๋ฐ”ํƒ•์œผ๋กœ LLM์ด ํ‰๊ฐ€ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ž๋™ ์ƒ์„ฑ
"""
import json
from typing import Dict, Any, List, Optional
from openai import OpenAI
import os
from dotenv import load_dotenv
load_dotenv()
class CustomCriteriaGenerator:
"""์‚ฌ์šฉ์ž ์ •์˜ ์‹ฌ์‚ฌ ๊ธฐ์ค€์œผ๋กœ LLM ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ํด๋ž˜์Šค"""
def __init__(self, api_key: Optional[str] = None):
self.api_key = api_key or os.getenv("OPENAI_API_KEY")
if not self.api_key:
raise ValueError("OPENAI_API_KEY๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
self.client = OpenAI(api_key=self.api_key)
self.model = "gpt-4o"
def generate_evaluation_prompt(
self,
criteria_name: str,
criteria_description: str,
sub_criteria: List[Dict[str, Any]],
total_points: int,
evaluation_target: str = "workflow" # "workflow" or "project_description"
) -> str:
"""
์‚ฌ์šฉ์ž ์ •์˜ ์‹ฌ์‚ฌ ๊ธฐ์ค€์„ ๋ฐ”ํƒ•์œผ๋กœ LLM ํ‰๊ฐ€ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ
Args:
criteria_name: ์‹ฌ์‚ฌ ๊ธฐ์ค€ ์ด๋ฆ„ (์˜ˆ: "๊ธฐ์ˆ ์  ์™„์„ฑ๋„")
criteria_description: ์‹ฌ์‚ฌ ๊ธฐ์ค€ ์ „์ฒด ์„ค๋ช…
sub_criteria: ์„ธ๋ถ€ ๊ธฐ์ค€ ๋ฆฌ์ŠคํŠธ [{"name": "์ด๋ฆ„", "description": "์„ค๋ช…", "points": ์ ์ˆ˜}, ...]
total_points: ์ด์ 
evaluation_target: ํ‰๊ฐ€ ๋Œ€์ƒ ์œ ํ˜• ("workflow" ๋˜๋Š” "project_description")
Returns:
์ƒ์„ฑ๋œ ํ‰๊ฐ€ ํ”„๋กฌํ”„ํŠธ ๋ฌธ์ž์—ด
"""
# ์„ธ๋ถ€ ๊ธฐ์ค€ ํฌ๋งทํŒ…
sub_criteria_text = ""
json_output_structure = {}
for idx, sub in enumerate(sub_criteria, 1):
sub_name = sub.get("name", f"๊ธฐ์ค€{idx}")
sub_desc = sub.get("description", "")
sub_points = sub.get("points", 0)
sub_details = sub.get("details", [])
sub_criteria_text += f"\n### {criteria_name[:2]}-{idx}. {sub_name} ({sub_points}์ )\n\n"
sub_criteria_text += f"**์„ค๋ช…:** {sub_desc}\n\n"
if sub_details:
sub_criteria_text += "**์„ธ๋ถ€ ํ‰๊ฐ€ ํ•ญ๋ชฉ:**\n"
detail_json = {}
for detail in sub_details:
detail_name = detail.get("name", "")
detail_points = detail.get("points", 0)
detail_desc = detail.get("description", "")
sub_criteria_text += f"- {detail_name} ({detail_points}์ ): {detail_desc}\n"
# JSON ํ‚ค๋ฅผ ํ•œ๊ธ€๋กœ ์œ ์ง€ํ•˜๋˜ ํŠน์ˆ˜๋ฌธ์ž ์ œ๊ฑฐ
safe_key = detail_name.replace(" ", "_").replace("/", "_").replace("ยท", "_")
detail_json[safe_key] = f"<0-{detail_points}>"
detail_json["์†Œ๊ณ„"] = f"<0-{sub_points}>"
detail_json["ํ‰๊ฐ€"] = "<๊ตฌ์ฒด์  ํ‰๊ฐ€>"
else:
detail_json = {
"์ ์ˆ˜": f"<0-{sub_points}>",
"ํ‰๊ฐ€": "<๊ตฌ์ฒด์  ํ‰๊ฐ€>"
}
# JSON ํ‚ค๋ฅผ ํ•œ๊ธ€๋กœ ์œ ์ง€
safe_sub_name = sub_name.replace(" ", "_").replace("/", "_").replace("ยท", "_")
json_output_structure[safe_sub_name] = detail_json
json_output_structure["์ด์ "] = f"<0-{total_points}>"
json_output_structure["์ข…ํ•ฉ_ํ‰๊ฐ€"] = "<์ „์ฒด์ ์ธ ํ‰๊ฐ€ ์š”์•ฝ>"
# ํ‰๊ฐ€ ๋Œ€์ƒ์— ๋”ฐ๋ฅธ ์•ˆ๋‚ด ๋ฌธ๊ตฌ
if evaluation_target == "workflow":
target_instruction = "์›Œํฌํ”Œ๋กœ์šฐ JSON ํŒŒ์ผ์„ ๋ถ„์„ํ•˜์—ฌ ๋‹ค์Œ ๊ธฐ์ค€์œผ๋กœ ํ‰๊ฐ€ํ•˜์‹ญ์‹œ์˜ค."
else:
target_instruction = "์ œ์ถœ๋œ ํ”„๋กœ์ ํŠธ ์„ค๋ช…์„ ๋ถ„์„ํ•˜์—ฌ ๋‹ค์Œ ๊ธฐ์ค€์œผ๋กœ ํ‰๊ฐ€ํ•˜์‹ญ์‹œ์˜ค."
# ์ตœ์ข… ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ
prompt = f"""# {criteria_name} ์‹ฌ์‚ฌ ๊ธฐ์ค€ ({total_points}์ )
โš ๏ธ **์ค‘์š”: ์—„๊ฒฉํ•œ ํ‰๊ฐ€ ์›์น™**
- ์ ์ˆ˜๋Š” ๋งค์šฐ ์—„๊ฒฉํ•˜๊ฒŒ ๋ถ€์—ฌํ•˜์‹ญ์‹œ์˜ค. ์™„๋ฒฝํ•˜์ง€ ์•Š์œผ๋ฉด ๊ฐ์ ํ•˜์‹ญ์‹œ์˜ค.
- "๊ฑฐ์˜ ์™„์„ฑ" ๋˜๋Š” "๋Œ€๋ถ€๋ถ„ ์ข‹์Œ"์€ ์ถฉ๋ถ„ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ธฐ์ค€์„ ์ •ํ™•ํžˆ ์ถฉ์กฑํ•ด์•ผ๋งŒ ์ ์ˆ˜๋ฅผ ๋ถ€์—ฌํ•˜์‹ญ์‹œ์˜ค.
- ๋ถˆํ™•์‹คํ•˜๊ฑฐ๋‚˜ ๋ช…ํ™•ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ๋Š” ๋‚ฎ์€ ์ ์ˆ˜๋ฅผ ๋ถ€์—ฌํ•˜์‹ญ์‹œ์˜ค.
- ๊ธฐ์ค€์„ ์™„์ „ํžˆ ์ถฉ์กฑํ•˜์ง€ ์•Š์œผ๋ฉด ํ•ด๋‹น ํ•ญ๋ชฉ์— ๋Œ€ํ•œ ์ ์ˆ˜๋ฅผ ๋ถ€์—ฌํ•˜์ง€ ๋งˆ์‹ญ์‹œ์˜ค.
{target_instruction}
## ์‹ฌ์‚ฌ ๊ธฐ์ค€ ์„ค๋ช…
{criteria_description}
## ํ‰๊ฐ€ ํ•ญ๋ชฉ:
{sub_criteria_text}
## ์ถœ๋ ฅ ํ˜•์‹:
JSON ํ˜•์‹์œผ๋กœ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ถœ๋ ฅํ•˜์‹ญ์‹œ์˜ค:
{json.dumps(json_output_structure, ensure_ascii=False, indent=4)}
"""
return prompt
def generate_prompt_with_llm(
self,
criteria_name: str,
criteria_description: str,
sub_criteria: List[Dict[str, Any]],
total_points: int,
evaluation_target: str = "workflow",
additional_context: str = ""
) -> str:
"""
LLM์„ ์‚ฌ์šฉํ•˜์—ฌ ๋” ์ •๊ตํ•œ ํ‰๊ฐ€ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ
Args:
criteria_name: ์‹ฌ์‚ฌ ๊ธฐ์ค€ ์ด๋ฆ„
criteria_description: ์‹ฌ์‚ฌ ๊ธฐ์ค€ ์„ค๋ช…
sub_criteria: ์„ธ๋ถ€ ๊ธฐ์ค€ ๋ฆฌ์ŠคํŠธ
total_points: ์ด์ 
evaluation_target: ํ‰๊ฐ€ ๋Œ€์ƒ
additional_context: ์ถ”๊ฐ€ ์ปจํ…์ŠคํŠธ (์„ ํƒ)
Returns:
LLM์ด ์ƒ์„ฑํ•œ ์ •๊ตํ•œ ํ‰๊ฐ€ ํ”„๋กฌํ”„ํŠธ
"""
user_input = f"""
๋‹ค์Œ ์‹ฌ์‚ฌ ๊ธฐ์ค€์„ ๋ฐ”ํƒ•์œผ๋กœ n8n ์›Œํฌํ”Œ๋กœ์šฐ ํ‰๊ฐ€์šฉ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ƒ์„ฑํ•ด์ฃผ์„ธ์š”.
## ์‹ฌ์‚ฌ ๊ธฐ์ค€ ์ •๋ณด
- ๊ธฐ์ค€ ์ด๋ฆ„: {criteria_name}
- ์ด์ : {total_points}์ 
- ํ‰๊ฐ€ ๋Œ€์ƒ: {"์›Œํฌํ”Œ๋กœ์šฐ JSON ํŒŒ์ผ" if evaluation_target == "workflow" else "ํ”„๋กœ์ ํŠธ ์„ค๋ช…์„œ"}
## ๊ธฐ์ค€ ์„ค๋ช…
{criteria_description}
## ์„ธ๋ถ€ ๊ธฐ์ค€
{json.dumps(sub_criteria, ensure_ascii=False, indent=2)}
## ์ถ”๊ฐ€ ์ปจํ…์ŠคํŠธ
{additional_context if additional_context else "์—†์Œ"}
์œ„ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ LLM์ด ํ‰๊ฐ€ํ•  ๋•Œ ์‚ฌ์šฉํ•  ์ƒ์„ธํ•˜๊ณ  ๊ตฌ์ฒด์ ์ธ ํ‰๊ฐ€ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ƒ์„ฑํ•ด์ฃผ์„ธ์š”.
ํ”„๋กฌํ”„ํŠธ์—๋Š” ๋‹ค์Œ์ด ํฌํ•จ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:
1. ์—„๊ฒฉํ•œ ํ‰๊ฐ€ ์›์น™
2. ๊ฐ ์„ธ๋ถ€ ๊ธฐ์ค€๋ณ„ ๊ตฌ์ฒด์ ์ธ ํ‰๊ฐ€ ํฌ์ธํŠธ
3. JSON ํ˜•์‹์˜ ์ถœ๋ ฅ ํ˜•์‹ ์ง€์ •
4. ํ‰๊ฐ€ ์‹œ ์ฃผ์˜์‚ฌํ•ญ
"""
try:
response = self.client.chat.completions.create(
model=self.model,
messages=[
{
"role": "system",
"content": """๋‹น์‹ ์€ n8n ์›Œํฌํ”Œ๋กœ์šฐ ํ‰๊ฐ€ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์„ค๊ณ„ํ•˜๋Š” ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค.
์‚ฌ์šฉ์ž๊ฐ€ ์ œ๊ณตํ•˜๋Š” ์‹ฌ์‚ฌ ๊ธฐ์ค€์„ ๋ฐ”ํƒ•์œผ๋กœ, LLM์ด ์›Œํฌํ”Œ๋กœ์šฐ๋‚˜ ํ”„๋กœ์ ํŠธ๋ฅผ ์—„๊ฒฉํ•˜๊ณ  ๊ณต์ •ํ•˜๊ฒŒ ํ‰๊ฐ€ํ•  ์ˆ˜ ์žˆ๋Š”
์ƒ์„ธํ•œ ํ‰๊ฐ€ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ ์‹œ ๋‹ค์Œ ์›์น™์„ ๋”ฐ๋ฅด์„ธ์š”:
1. ๊ฐ ํ‰๊ฐ€ ํ•ญ๋ชฉ๋ณ„๋กœ ๊ตฌ์ฒด์ ์ธ ํŒ๋‹จ ๊ธฐ์ค€ ์ œ์‹œ
2. JSON ํ˜•์‹์œผ๋กœ ์ ์ˆ˜์™€ ํ‰๊ฐ€๋ฅผ ์ถœ๋ ฅํ•˜๋„๋ก ์ง€์‹œ
3. ์—„๊ฒฉํ•œ ํ‰๊ฐ€ ์›์น™ ๋ช…์‹œ
4. ์›Œํฌํ”Œ๋กœ์šฐ JSON์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌ์ฒด์ ์ธ ์‹ ํ˜ธ(signal) ์ œ์‹œ
5. ํ‰๊ฐ€์ž๊ฐ€ ์ผ๊ด€๋˜๊ฒŒ ์ ์ˆ˜๋ฅผ ๋ถ€์—ฌํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ช…ํ™•ํ•œ ๊ธฐ์ค€ ์ œ๊ณต"""
},
{
"role": "user",
"content": user_input
}
],
temperature=0.3
)
return response.choices[0].message.content
except Exception as e:
print(f"LLM ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ ์‹คํŒจ: {e}")
# ์‹คํŒจ ์‹œ ๊ธฐ๋ณธ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ ๋ฉ”์„œ๋“œ ์‚ฌ์šฉ
return self.generate_evaluation_prompt(
criteria_name, criteria_description, sub_criteria,
total_points, evaluation_target
)
def validate_criteria(self, sub_criteria: List[Dict[str, Any]], total_points: int) -> Dict[str, Any]:
"""
์‹ฌ์‚ฌ ๊ธฐ์ค€์˜ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
Args:
sub_criteria: ์„ธ๋ถ€ ๊ธฐ์ค€ ๋ฆฌ์ŠคํŠธ
total_points: ๋ช…์‹œ๋œ ์ด์ 
Returns:
๊ฒ€์ฆ ๊ฒฐ๊ณผ {"valid": bool, "message": str, "calculated_total": int}
"""
calculated_total = 0
for sub in sub_criteria:
sub_points = sub.get("points", 0)
details = sub.get("details", [])
if details:
detail_total = sum(d.get("points", 0) for d in details)
if detail_total != sub_points:
return {
"valid": False,
"message": f"'{sub.get('name', '?')}'์˜ ์„ธ๋ถ€ ์ ์ˆ˜ ํ•ฉ๊ณ„({detail_total})๊ฐ€ ์†Œ๊ณ„({sub_points})์™€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.",
"calculated_total": calculated_total
}
calculated_total += sub_points
if calculated_total != total_points:
return {
"valid": False,
"message": f"์„ธ๋ถ€ ๊ธฐ์ค€ ์ ์ˆ˜ ํ•ฉ๊ณ„({calculated_total})๊ฐ€ ์ด์ ({total_points})๊ณผ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.",
"calculated_total": calculated_total
}
return {
"valid": True,
"message": "์‹ฌ์‚ฌ ๊ธฐ์ค€์ด ์œ ํšจํ•ฉ๋‹ˆ๋‹ค.",
"calculated_total": calculated_total
}
# ๊ธฐ๋ณธ ํ…œํ”Œ๋ฆฟ ์ œ๊ณต (๊ณ ์ • ํ…œํ”Œ๋ฆฟ - ๋ฐฐ์ ๋งŒ ์กฐ์ • ๊ฐ€๋Šฅ)
FIXED_CRITERIA_TEMPLATES = {
"๊ธฐ์ˆ ์ _์™„์„ฑ๋„": {
"name": "๊ธฐ์ˆ ์  ์™„์„ฑ๋„",
"description": "์›Œํฌํ”Œ๋กœ์šฐ์˜ ๊ตฌ์กฐ์  ์™„์„ฑ๋„, ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ์˜ ์ผ๊ด€์„ฑ, ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ํ‰๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.",
"evaluation_target": "workflow",
"structure": [
{
"name": "๊ตฌ์กฐ ์™„๊ฒฐ์„ฑ & ๋ชจ๋“ˆํ™”",
"description": "Triggerโ†’Output๊นŒ์ง€ ๊ฒฝ๋กœ ์™„์„ฑ, dead node ์—†์Œ, ์—ญํ•  ๊ธฐ๋ฐ˜ ๋„ค์ด๋ฐยท๋ชจ๋“ˆํ™”",
"ratio": 0.4 # 40%
},
{
"name": "๋ฐ์ดํ„ฐยท์Šคํ‚ค๋งˆ ์ผ๊ด€์„ฑ",
"description": "key ์ผ๊ด€์„ฑ, ๋ถˆํ•„์š”ํ•œ overwrite ์—†์Œ, merge ์ •ํ•ฉ์„ฑ",
"ratio": 0.4 # 40%
},
{
"name": "๋ถ„๊ธฐ/์˜ˆ์™ธ ์ฒ˜๋ฆฌ",
"description": "์˜๋ฏธ ์žˆ๋Š” ์กฐ๊ฑด ๋ถ„๊ธฐ, fallback/default ๊ฒฝ๋กœ ์กด์žฌ",
"ratio": 0.2 # 20%
}
]
},
"์—…์Šคํ…Œ์ด์ง€_ํ™œ์šฉ๋„": {
"name": "์—…์Šคํ…Œ์ด์ง€ ์ œํ’ˆ ํ™œ์šฉ๋„",
"description": "์—…์Šคํ…Œ์ด์ง€ API์˜ ์ ์ ˆํ•œ ์‚ฌ์šฉ, ํ”„๋กฌํ”„ํŠธ ์„ค๊ณ„, ๊ธฐ๋Šฅ ์กฐํ•ฉ์„ ํ‰๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.",
"evaluation_target": "workflow",
"structure": [
{
"name": "API ์‚ฌ์šฉ ์ ํ•ฉ์„ฑ",
"description": "๊ณต์‹ ์—”๋“œํฌ์ธํŠธ, credential ๋ณด์•ˆ, ๋ชฉ์  ์ ํ•ฉ API ์„ ํƒ",
"ratio": 0.33 # 33%
},
{
"name": "Prompt/System ์„ค๊ณ„",
"description": "๋ช…ํ™•ํ•œ ์—ญํ•  ์ •์˜, ์ถœ๋ ฅ ํฌ๋งท ๊ฐ•์ œ๋ ฅ, ๊น”๋”ํ•œ ํ”„๋กฌํ”„ํŠธ",
"ratio": 0.33 # 33%
},
{
"name": "๊ธฐ๋Šฅ ์กฐํ•ฉยท์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜",
"description": "๋‹ค๋‹จ๊ณ„ ํŒŒ์ดํ”„๋ผ์ธ, ์ „/ํ›„์ฒ˜๋ฆฌ, ํšจ์œจ์  ํ˜ธ์ถœ",
"ratio": 0.34 # 34%
}
]
}
}
# ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ๊ฐ€๋Šฅํ•œ ํ…œํ”Œ๋ฆฟ
DEFAULT_CRITERIA_TEMPLATES = {
"์‹ค์šฉ์„ฑ": {
"name": "์‹ค์šฉ์„ฑ",
"description": "์‹ค์ œ ์—…๋ฌด ์ ์šฉ ๊ฐ€๋Šฅ์„ฑ, ์žฌ์‚ฌ์šฉ์„ฑยทํ™•์žฅ์„ฑ, ์‚ฌ์šฉ์ž ํŽธ์˜์„ฑ์„ ํ‰๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.",
"total_points": 30,
"evaluation_target": "project_description",
"sub_criteria": [
{
"name": "์—…๋ฌด ์ ์šฉ ๊ฐ€๋Šฅ์„ฑ",
"description": "๋ฐ˜๋ณต ์—…๋ฌด ์ œ๊ฑฐ, ์‹ค์ œ ์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค ์„ ๋ช…, ์‹œ๊ฐ„ ์ ˆ๊ฐ ํšจ๊ณผ ๋ช…ํ™•",
"points": 10,
"details": [
{"name": "๋ฐ˜๋ณต ์—…๋ฌด ์ œ๊ฑฐ", "points": 3, "description": "๋งค์ผ/๋งค์ฃผ ๋ฐ˜๋ณต๋˜๋Š” ์ž‘์—…์„ ์ž๋™ํ™”"},
{"name": "์‹ค์ œ ์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค ์„ ๋ช…", "points": 4, "description": "๋ˆ„๊ฐ€/์–ธ์ œ/์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•˜๋Š”์ง€ ๊ตฌ์ฒด์ "},
{"name": "์‹œ๊ฐ„ ์ ˆ๊ฐ ํšจ๊ณผ ๋ช…ํ™•", "points": 3, "description": "์ •๋Ÿ‰์ /์งˆ์  ํšจ๊ณผ ์ œ์‹œ"}
]
},
{
"name": "์žฌ์‚ฌ์šฉ์„ฑยทํ™•์žฅ์„ฑ",
"description": "๋‹ค๋ฅธ ๋ฐ์ดํ„ฐยท์กฐ์ง์—๋„ ์ ์šฉ ๊ฐ€๋Šฅํ•œ ์ผ๋ฐ˜ํ™” ๊ตฌ์กฐ",
"points": 10,
"details": [
{"name": "๋‹ค๋ฅธ ๋ฐ์ดํ„ฐยท์กฐ์ง ์ ์šฉ ๊ฐ€๋Šฅ", "points": 5, "description": "ํŠน์ • ํšŒ์‚ฌ/ํŒ€ ์ข…์† ์ตœ์†Œํ™”"},
{"name": "ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์„ค๊ณ„", "points": 5, "description": "์ƒˆ๋กœ์šด ์š”๊ตฌ์‚ฌํ•ญ ์ถ”๊ฐ€ ์‹œ ์ตœ์†Œ ์ˆ˜์ •"}
]
},
{
"name": "์‚ฌ์šฉ์ž ํŽธ์˜์„ฑ",
"description": "์ตœ์ข… ์‚ฌ์šฉ์ž ์ž…์žฅ์—์„œ ์ง๊ด€์ ์œผ๋กœ ์“ธ ์ˆ˜ ์žˆ๋Š”๊ฐ€?",
"points": 10,
"details": [
{"name": "์ง๊ด€์ ์ธ ์ž…๋ ฅ ๋ฐฉ์‹", "points": 4, "description": "๊ฐ„๋‹จํ•˜๊ณ  ๋ช…ํ™•ํ•œ ์ž…๋ ฅ"},
{"name": "๋ช…ํ™•ํ•œ ์ถœ๋ ฅ ๋ฐ ํ”ผ๋“œ๋ฐฑ", "points": 3, "description": "์ดํ•ดํ•˜๊ธฐ ์‰ฌ์šด ๊ฒฐ๊ณผ๋ฌผ"},
{"name": "์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐ ๊ฐ€์ด๋“œ", "points": 3, "description": "์‚ฌ์šฉ์ž ์นœํ™”์  ๋ฉ”์‹œ์ง€"}
]
}
]
},
"๋ฌธ์ œํ•ด๊ฒฐ": {
"name": "๋ฌธ์ œ ํ•ด๊ฒฐ ์ ‘๊ทผ๋ฒ•",
"description": "๋ฌธ์ œ ์ •์˜์˜ ๋…์ฐฝ์„ฑ๊ณผ ์†”๋ฃจ์…˜์˜ ์ฐธ์‹ ์„ฑ์„ ํ‰๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.",
"total_points": 30,
"evaluation_target": "project_description",
"sub_criteria": [
{
"name": "๋ฌธ์ œ ์ •์˜์˜ ๋…์ฐฝ์„ฑ",
"description": "์ž๋™ํ™” ๋Œ€์ƒ ์„ ์ •์ด ์ƒˆ๋กญ๊ฑฐ๋‚˜, ์‹ค์งˆ์  ๋ฌธ์ œ๋ฅผ ์ž˜ ํฌ์ฐฉํ–ˆ๋Š”๊ฐ€? ๊ธฐ์กด ๋ฐฉ๋ฒ•๊ณผ ์ฐจ๋ณ„ํ™”๋œ ์ ‘๊ทผ์„ ํ–ˆ๋Š”๊ฐ€?",
"points": 15,
"details": [
{"name": "์‹ค์งˆ์  ๋ฌธ์ œ ํฌ์ฐฉ", "points": 8, "description": "๊ตฌ์ฒด์  ๋ฌธ์ œ ์ƒํ™ฉ ์ œ์‹œ, ๋ช…ํ™•ํ•œ pain point"},
{"name": "์ฐจ๋ณ„ํ™”๋œ ๊ด€์  ๋ฐ ๋…์ฐฝ์„ฑ", "points": 7, "description": "๊ธฐ์กด ๋ฐฉ์‹์˜ ํ•œ๊ณ„ ๋ถ„์„, ์ƒˆ๋กœ์šด ์‹œ๊ฐ"}
]
},
{
"name": "์†”๋ฃจ์…˜ ์ฐธ์‹ ์„ฑ",
"description": "๋‹จ์ˆœ API ๋‚˜์—ด์ด ์•„๋‹Œ, ์ƒˆ๋กœ์šด ์ ‘๊ทผยท์ „๋žต ๊ธฐ๋ฐ˜ ์„ค๊ณ„",
"points": 15,
"details": [
{"name": "์ „๋žต์  ์กฐํ•ฉ ๋ฐ ๋ณต์žก๋„", "points": 8, "description": "๋‹ค๋‹จ๊ณ„ ๋กœ์ง, ๋…ผ๋ฆฌ์  ์—ฐ๊ฒฐ"},
{"name": "์ฐฝ์˜์  ์„ค๊ณ„", "points": 7, "description": "๋ฌธ์ œ ํ•ด๊ฒฐ ํŠนํ™” ์„ค๊ณ„, ๋…ํŠนํ•œ ํŒจํ„ด"}
]
}
]
}
}
def get_fixed_template_with_points(template_name: str, total_points: int) -> Optional[Dict[str, Any]]:
"""
๊ณ ์ • ํ…œํ”Œ๋ฆฟ์„ ๋ฐฐ์ ์— ๋งž๊ฒŒ ์ƒ์„ฑ
Args:
template_name: "๊ธฐ์ˆ ์ _์™„์„ฑ๋„" ๋˜๋Š” "์—…์Šคํ…Œ์ด์ง€_ํ™œ์šฉ๋„"
total_points: ์ด ๋ฐฐ์ 
Returns:
๋ฐฐ์ ์ด ์ ์šฉ๋œ ํ…œํ”Œ๋ฆฟ ๋”•์…”๋„ˆ๋ฆฌ
"""
template = FIXED_CRITERIA_TEMPLATES.get(template_name)
if not template:
return None
# ๋น„์œจ์— ๋”ฐ๋ผ ๋ฐฐ์  ๊ณ„์‚ฐ
sub_criteria = []
remaining_points = total_points
for idx, item in enumerate(template["structure"]):
# ๋งˆ์ง€๋ง‰ ํ•ญ๋ชฉ์€ ๋‚จ์€ ์ ์ˆ˜ ๋ชจ๋‘ ํ• ๋‹น (๋ฐ˜์˜ฌ๋ฆผ ์˜ค์ฐจ ๋ฐฉ์ง€)
if idx == len(template["structure"]) - 1:
points = remaining_points
else:
points = round(total_points * item["ratio"])
remaining_points -= points
sub_criteria.append({
"name": item["name"],
"description": item["description"],
"points": points,
"details": []
})
return {
"name": template["name"],
"description": template["description"],
"total_points": total_points,
"evaluation_target": template["evaluation_target"],
"sub_criteria": sub_criteria
}
def get_default_template(template_name: str) -> Optional[Dict[str, Any]]:
"""๊ธฐ๋ณธ ํ…œํ”Œ๋ฆฟ ๊ฐ€์ ธ์˜ค๊ธฐ"""
return DEFAULT_CRITERIA_TEMPLATES.get(template_name)
def list_default_templates() -> List[str]:
"""์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๊ธฐ๋ณธ ํ…œํ”Œ๋ฆฟ ๋ชฉ๋ก"""
return list(DEFAULT_CRITERIA_TEMPLATES.keys())
def list_fixed_templates() -> List[str]:
"""๊ณ ์ • ํ…œํ”Œ๋ฆฟ ๋ชฉ๋ก"""
return list(FIXED_CRITERIA_TEMPLATES.keys())