clovax-tax-chatbot / llm_processor.py
bissal's picture
Complete RAGโ†’LLMโ†’Web pipeline integration ๐Ÿค– Generated with Claude Code
4aa05c6
# llm_processor.py - LLM ์ฒ˜๋ฆฌ ๋ชจ๋“ˆ
import os
import re
import time
from datetime import datetime
import logging
# HuggingFace ๊ด€๋ จ import
try:
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
LlamaConfig,
LlamaForCausalLM,
BitsAndBytesConfig
)
import torch
TRANSFORMERS_AVAILABLE = True
except ImportError:
print("โš ๏ธ Transformers ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์„ค์น˜๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค")
TRANSFORMERS_AVAILABLE = False
class TaxRuleEngine:
"""์ทจ๋“์„ธ ๊ณ„์‚ฐ ์—”์ง„ (๋…ธํŠธ๋ถ์—์„œ ์ถ”์ถœ)"""
def __init__(self):
# ์กฐ์ •๋Œ€์ƒ์ง€์—ญ (์„œ์šธ ์ฃผ์š” ์ง€์—ญ)
self.adjustment_areas = [
"๊ฐ•๋‚จ๊ตฌ", "์„œ์ดˆ๊ตฌ", "์†กํŒŒ๊ตฌ", "์šฉ์‚ฐ๊ตฌ"
]
# ๋‹ค์ฃผํƒ ์ค‘๊ณผ์„ธ ์„ธ์œจ (์ฒœ๋ถ„์˜)
self.multi_housing_rates = {
"1์„ธ๋Œ€2์ฃผํƒ_์กฐ์ •๋Œ€์ƒ": 80, # 8%
"1์„ธ๋Œ€3์ฃผํƒ_์กฐ์ •๋Œ€์ƒ": 120, # 12%
"1์„ธ๋Œ€4์ฃผํƒ์ด์ƒ_์กฐ์ •๋Œ€์ƒ": 120, # 12%
"1์„ธ๋Œ€3์ฃผํƒ_์กฐ์ •๋Œ€์ƒ์™ธ": 80, # 8%
"1์„ธ๋Œ€4์ฃผํƒ์ด์ƒ_์กฐ์ •๋Œ€์ƒ์™ธ": 120, # 12%
}
def calculate_housing_tax_rate(self, acquisition_value):
"""์ฃผํƒ ์ทจ๋“์„ธ์œจ ๊ณ„์‚ฐ (์ง€๋ฐฉ์„ธ๋ฒ• ์ œ11์กฐ ์ œ8ํ˜ธ)"""
if acquisition_value <= 600000000: # 6์–ต์› ์ดํ•˜
return 10
elif acquisition_value <= 900000000: # 6์–ต ์ดˆ๊ณผ 9์–ต ์ดํ•˜
excess = acquisition_value - 600000000
rate = (excess / 300000000) * 20 + 10
return round(rate, 4)
else: # 9์–ต ์ดˆ๊ณผ
return 30
def is_adjustment_area(self, location):
"""์กฐ์ •๋Œ€์ƒ์ง€์—ญ ์—ฌ๋ถ€ ํŒ๋‹จ"""
return any(area in location for area in self.adjustment_areas)
def determine_multi_housing_heavy_tax(self, total_housing_count, is_adjustment_area, acquisition_type="๋งค๋งค"):
"""๋‹ค์ฃผํƒ ์ค‘๊ณผ์„ธ ์œ ํ˜• ๊ฒฐ์ •"""
if acquisition_type in ['์ƒ์†', '์ฆ์—ฌ', '๋ฌด์ƒ์ทจ๋“']:
if is_adjustment_area and total_housing_count >= 2:
return '์กฐ์ •์ง€์—ญ๊ณ ๊ฐ€์ฃผํƒ์ฆ์—ฌ' # 12%
return None
if total_housing_count <= 1:
return None
elif total_housing_count == 2:
return '1์„ธ๋Œ€2์ฃผํƒ_์กฐ์ •๋Œ€์ƒ' if is_adjustment_area else None
elif total_housing_count == 3:
return '1์„ธ๋Œ€3์ฃผํƒ_์กฐ์ •๋Œ€์ƒ' if is_adjustment_area else '1์„ธ๋Œ€3์ฃผํƒ_์กฐ์ •๋Œ€์ƒ์™ธ'
else: # 4์ฃผํƒ ์ด์ƒ
return '1์„ธ๋Œ€4์ฃผํƒ์ด์ƒ_์กฐ์ •๋Œ€์ƒ' if is_adjustment_area else '1์„ธ๋Œ€4์ฃผํƒ์ด์ƒ_์กฐ์ •๋Œ€์ƒ์™ธ'
def calculate_comprehensive_tax(self, property_info):
"""์ข…ํ•ฉ ์ทจ๋“์„ธ ๊ณ„์‚ฐ"""
if not property_info.get('acquisition_value'):
return None
# ๊ธฐ๋ณธ ์„ธ์œจ ๊ณ„์‚ฐ
base_rate = self.calculate_housing_tax_rate(property_info['acquisition_value'])
# ์ฃผํƒ์ˆ˜ ๋ฐ ์กฐ์ •๋Œ€์ƒ์ง€์—ญ ํ™•์ธ
total_housing_count = len(property_info.get('housing_list', [])) + 1
is_adjustment_area = self.is_adjustment_area(property_info.get('location', ''))
# ์ค‘๊ณผ์„ธ ๊ฒฐ์ •
heavy_tax_type = property_info.get('heavy_tax_type')
if not heavy_tax_type:
heavy_tax_type = self.determine_multi_housing_heavy_tax(
total_housing_count,
is_adjustment_area,
property_info.get('acquisition_type', '๋งค๋งค')
)
# ์ตœ์ข… ์„ธ์œจ ๊ฒฐ์ •
final_rate = base_rate
if heavy_tax_type and heavy_tax_type in self.multi_housing_rates:
final_rate = self.multi_housing_rates[heavy_tax_type]
elif heavy_tax_type == '์กฐ์ •์ง€์—ญ๊ณ ๊ฐ€์ฃผํƒ์ฆ์—ฌ':
final_rate = 120 # 12%
# ๋ฉด์„ธ์  ํ™•์ธ (50๋งŒ์› ์ดํ•˜)
if property_info['acquisition_value'] <= 500000:
tax_amount = 0
else:
tax_amount = int(property_info['acquisition_value'] * (final_rate / 1000))
return {
'tax_amount': tax_amount,
'base_rate': base_rate,
'final_rate': final_rate,
'heavy_tax_type': heavy_tax_type,
'is_adjustment_area': is_adjustment_area,
'total_housing_count': total_housing_count,
'acquisition_value': property_info['acquisition_value']
}
class LLMProcessor:
"""HyperCLOVA X ๊ธฐ๋ฐ˜ LLM ์ฒ˜๋ฆฌ ๋ชจ๋“ˆ"""
def __init__(self):
self.model = None
self.tokenizer = None
self.tax_engine = TaxRuleEngine()
self.is_initialized = False
self.device = 'cpu'
# ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ
self.system_prompt = """๋‹น์‹ ์€ ๋Œ€ํ•œ๋ฏผ๊ตญ ์ง€๋ฐฉ์„ธ๋ฒ• ์ทจ๋“์„ธ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค.
์ฃผ์š” ์—ญํ• :
1. ์ทจ๋“์„ธ ๊ด€๋ จ ์งˆ๋ฌธ์— ์ •ํ™•ํ•˜๊ณ  ์ƒ์„ธํ•œ ๋‹ต๋ณ€ ์ œ๊ณต
2. ์ง€๋ฐฉ์„ธ๋ฒ• ์ œ2์žฅ ์ทจ๋“์„ธ ๊ทœ์ • ๊ธฐ์ค€ ํ•ด์„
3. ๋‹ค์ฃผํƒ ๋ณด์œ ์‹œ ์ค‘๊ณผ์„ธ ๊ณ„์‚ฐ ๋ฐ ์„ค๋ช…
4. ์กฐ์ •๋Œ€์ƒ์ง€์—ญ ์—ฌ๋ถ€์— ๋”ฐ๋ฅธ ์„ธ์œจ ์ฐจ์ด ์„ค๋ช…
5. ์ฃผํƒ์ˆ˜ ์‚ฐ์ • ๊ธฐ์ค€ (์‹œํ–‰๋ น ์ œ28์กฐ์˜4) ์ ์šฉ
๋‹ต๋ณ€ ํ˜•์‹:
- ํ•ด๋‹น ๋ฒ•๋ น ์กฐํ•ญ ๋ช…์‹œ
- ๊ตฌ์ฒด์ ์ธ ๊ณ„์‚ฐ ๊ณผ์ • ์„ค๋ช…
- ์ ˆ์„ธ ๋ฐฉ์•ˆ ์ œ์‹œ (ํ•ฉ๋ฒ•์  ๋ฒ”์œ„ ๋‚ด)
- ์‹ ๊ณ  ๊ธฐํ•œ ๋ฐ ์œ ์˜์‚ฌํ•ญ ์•ˆ๋‚ด
์ „๋ฌธ์ ์ด๊ณ  ์นœ์ ˆํ•œ ํ†ค์œผ๋กœ ๋‹ต๋ณ€ํ•˜์„ธ์š”."""
def initialize_model(self, force_cpu=False):
"""HyperCLOVA X ๋ชจ๋ธ ์ดˆ๊ธฐํ™”"""
if not TRANSFORMERS_AVAILABLE:
print("โŒ Transformers ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ค์น˜ํ•ด์ฃผ์„ธ์š”: pip install transformers torch")
return False
if self.is_initialized:
return True
print("๐Ÿ”„ HyperCLOVA X 1.5B ๋ชจ๋ธ ์ดˆ๊ธฐํ™” ์ค‘...")
try:
# HuggingFace ํ† ํฐ ํ™•์ธ
hf_token = os.getenv('HF_TOKEN') or os.getenv('HUGGINGFACE_HUB_TOKEN')
if not hf_token:
print("โš ๏ธ HuggingFace ํ† ํฐ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค")
return False
# ๋””๋ฐ”์ด์Šค ์„ค์ •
if force_cpu or not torch.cuda.is_available():
self.device = 'cpu'
print("๐Ÿ’ป CPU ๋ชจ๋“œ๋กœ ์‹คํ–‰")
else:
self.device = 'cuda'
print(f"๐Ÿ”ฅ GPU ๋ชจ๋“œ๋กœ ์‹คํ–‰: {torch.cuda.get_device_name()}")
model_name = "naver-hyperclovax/HyperCLOVAX-SEED-Text-Instruct-1.5B"
# Config ๋กœ๋“œ
config = LlamaConfig.from_pretrained(model_name, token=hf_token)
# Tokenizer ๋กœ๋“œ
self.tokenizer = AutoTokenizer.from_pretrained(
model_name,
token=hf_token,
legacy=False,
add_eos_token=True,
add_bos_token=True
)
if self.tokenizer.pad_token is None:
self.tokenizer.pad_token = self.tokenizer.eos_token
# ๋ชจ๋ธ ๋กœ๋“œ
if self.device == 'cuda':
# GPU: 8bit ์–‘์žํ™”
quantization_config = BitsAndBytesConfig(
load_in_8bit=True,
llm_int8_enable_fp32_cpu_offload=True,
llm_int8_threshold=6.0
)
self.model = LlamaForCausalLM.from_pretrained(
model_name,
config=config,
quantization_config=quantization_config,
torch_dtype=torch.float16,
device_map="auto",
token=hf_token,
low_cpu_mem_usage=True
)
else:
# CPU: float32
self.model = LlamaForCausalLM.from_pretrained(
model_name,
config=config,
torch_dtype=torch.float32,
token=hf_token,
low_cpu_mem_usage=True
)
self.model = self.model.to('cpu')
self.is_initialized = True
print(f"โœ… HyperCLOVA X ๋ชจ๋ธ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ ({self.device})")
return True
except Exception as e:
print(f"โŒ ๋ชจ๋ธ ์ดˆ๊ธฐํ™” ์‹คํŒจ: {e}")
return False
def extract_property_info(self, user_input):
"""์‚ฌ์šฉ์ž ์ž…๋ ฅ์—์„œ ๋ถ€๋™์‚ฐ ์ •๋ณด ์ž๋™ ์ถ”์ถœ"""
property_info = {
'property_type': '์ฃผํƒ',
'acquisition_type': '๋งค๋งค',
'acquisition_value': None,
'location': '',
'housing_list': []
}
# ๊ธˆ์•ก ์ถ”์ถœ (๋‹ค์–‘ํ•œ ๋‹จ์œ„ ์ง€์›)
amount_patterns = [
(r'(\d+(?:\.\d+)?)์–ต', 100000000),
(r'(\d+(?:,\d+)?)๋งŒ์›', 10000),
]
for pattern, multiplier in amount_patterns:
amounts = re.findall(pattern, user_input)
if amounts:
amount_str = amounts[0].replace(',', '')
property_info['acquisition_value'] = int(float(amount_str) * multiplier)
break
# ์ง€์—ญ ์ถ”์ถœ
for area in self.tax_engine.adjustment_areas:
area_name = area.replace('๊ตฌ', '')
if area_name in user_input or area in user_input:
property_info['location'] = f'์„œ์šธํŠน๋ณ„์‹œ {area}'
break
# ์ฃผํƒ์ˆ˜ ์ถ”์ถœ
housing_patterns = [r'(\d+)์ฃผํƒ', r'๊ธฐ์กด.*?(\d+).*?์ฃผํƒ', r'(\d+).*?๋ณด์œ ']
for pattern in housing_patterns:
matches = re.findall(pattern, user_input)
if matches:
existing_count = int(matches[0]) - 1
for i in range(max(0, existing_count)):
property_info['housing_list'].append({
'id': f'existing_house_{i+1}',
'type': '์ฃผํƒ',
'acquisition_type': '๋งค๋งค',
'value': 500000000
})
break
return property_info
def format_tax_result(self, result, property_info):
"""๊ณ„์‚ฐ ๊ฒฐ๊ณผ๋ฅผ ์‚ฌ์šฉ์ž ์นœํ™”์ ์œผ๋กœ ํฌ๋งทํŒ…"""
if not result:
return "๐Ÿ“‹ ์ •ํ™•ํ•œ ๊ณ„์‚ฐ์„ ์œ„ํ•ด ๋ถ€๋™์‚ฐ ๊ฐ€๊ฒฉ์„ ๊ตฌ์ฒด์ ์œผ๋กœ ์•Œ๋ ค์ฃผ์‹œ๋ฉด ๋„์›€์ด ๋ฉ๋‹ˆ๋‹ค."
output = f"""๐Ÿ“‹ **์ทจ๋“์„ธ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ**
โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
๐Ÿ  **์ทจ๋“๊ฐ€์•ก**: {result['acquisition_value']:,}์›
๐Ÿ˜๏ธ **์ด ์ฃผํƒ์ˆ˜**: {result['total_housing_count']}์ฃผํƒ
๐Ÿ“ **์กฐ์ •๋Œ€์ƒ์ง€์—ญ**: {'์˜ˆ' if result['is_adjustment_area'] else '์•„๋‹ˆ์˜ค'}
๐Ÿ’ฐ **์„ธ์œจ ์ •๋ณด**
โ€ข ๊ธฐ๋ณธ์„ธ์œจ: {result['base_rate']}โ€ฐ ({result['base_rate']/10:.1f}%)
โ€ข ์ตœ์ข…์„ธ์œจ: {result['final_rate']}โ€ฐ ({result['final_rate']/10:.1f}%)
๐Ÿ’ธ **์ทจ๋“์„ธ์•ก**: {result['tax_amount']:,}์›"""
if result['heavy_tax_type']:
output += f"\nโš ๏ธ **์ค‘๊ณผ์„ธ ์ ์šฉ**: {result['heavy_tax_type']}"
output += f"""\n\n๐Ÿ“œ **๋ฒ•๋ น ๊ทผ๊ฑฐ**
โ€ข ์ง€๋ฐฉ์„ธ๋ฒ• ์ œ11์กฐ (๋ถ€๋™์‚ฐ ์ทจ๋“์„ธ)
โ€ข ์ง€๋ฐฉ์„ธ๋ฒ• ์ œ13์กฐ (์ค‘๊ณผ์„ธ)
โ€ข ์ง€๋ฐฉ์„ธ๋ฒ• ์‹œํ–‰๋ น ์ œ28์กฐ์˜4 (์ฃผํƒ์ˆ˜ ์‚ฐ์ •)
โ€ข ์‹ ๊ณ ๊ธฐํ•œ: ์ทจ๋“์ผ๋กœ๋ถ€ํ„ฐ 60์ผ ์ด๋‚ด"""
return output
def generate_ai_response(self, user_input, rag_context="", max_length=300):
"""AI ์‘๋‹ต ์ƒ์„ฑ (RAG ์ปจํ…์ŠคํŠธ ํฌํ•จ)"""
if not self.is_initialized:
print("โš ๏ธ ๋ชจ๋ธ์ด ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ดˆ๊ธฐํ™”๋ฅผ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค...")
if not self.initialize_model():
return "โŒ AI ๋ชจ๋ธ ์ดˆ๊ธฐํ™”์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."
try:
# 1. ์ž๋™ ๊ณ„์‚ฐ
property_info = self.extract_property_info(user_input)
tax_result = None
tax_summary = ""
if property_info.get('acquisition_value'):
property_info['acquisition_date'] = datetime.now().strftime('%Y-%m-%d')
tax_result = self.tax_engine.calculate_comprehensive_tax(property_info)
tax_summary = self.format_tax_result(tax_result, property_info)
# 2. AI ๋‹ต๋ณ€ ์ƒ์„ฑ์„ ์œ„ํ•œ ํ”„๋กฌํ”„ํŠธ ๊ตฌ์„ฑ
context_parts = []
if rag_context:
context_parts.append(f"์ฐธ๊ณ  ์ž๋ฃŒ:\n{rag_context}")
if tax_summary:
context_parts.append(f"์ž๋™ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ:\n{tax_summary}")
context_prompt = f"""{self.system_prompt}
์‚ฌ์šฉ์ž ์งˆ๋ฌธ: {user_input}
{chr(10).join(context_parts)}
์œ„ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์ „๋ฌธ๊ฐ€๋กœ์„œ ์ƒ์„ธํ•˜๊ณ  ์ดํ•ดํ•˜๊ธฐ ์‰ฌ์šด ์„ค๋ช…์„ ์ œ๊ณตํ•ด์ฃผ์„ธ์š”:"""
# 3. ํ† ํฌ๋‚˜์ด์ง•
inputs = self.tokenizer(
context_prompt,
return_tensors="pt",
max_length=1800,
truncation=True
).to(self.model.device)
# 4. AI ์‘๋‹ต ์ƒ์„ฑ
with torch.no_grad():
outputs = self.model.generate(
inputs.input_ids,
attention_mask=inputs.attention_mask,
max_new_tokens=max_length,
do_sample=True,
temperature=0.6,
top_p=0.85,
repetition_penalty=1.15,
pad_token_id=self.tokenizer.pad_token_id,
eos_token_id=self.tokenizer.eos_token_id
)
# 5. ์‘๋‹ต ๋””์ฝ”๋”ฉ
generated_response = self.tokenizer.decode(
outputs[0][inputs.input_ids.shape[1]:],
skip_special_tokens=True
).strip()
# 6. ์ตœ์ข… ์‘๋‹ต ๊ตฌ์„ฑ
final_response = ""
if tax_summary:
final_response += f"{tax_summary}\n\n"
final_response += f"""๐Ÿค– **AI ์ „๋ฌธ๊ฐ€ ์ƒ์„ธ ์„ค๋ช…**
โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
{generated_response}
---
๐Ÿ’ก **์ถ”๊ฐ€ ๋ฌธ์˜๋‚˜ ๋‹ค๋ฅธ ์ƒํ™ฉ์— ๋Œ€ํ•œ ์ƒ๋‹ด์ด ํ•„์š”ํ•˜์‹œ๋ฉด ์–ธ์ œ๋“  ๋ง์”€ํ•ด ์ฃผ์„ธ์š”!**"""
return final_response
except Exception as e:
error_response = f"โŒ AI ์‘๋‹ต ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}\n\n"
if tax_summary:
return error_response + tax_summary
return error_response + "๊ธฐ๋ณธ์ ์ธ ์ทจ๋“์„ธ ์ •๋ณด๋Š” ์ง€๋ฐฉ์„ธ๋ฒ• ์ œ11์กฐ๋ฅผ ์ฐธ๊ณ ํ•˜์„ธ์š”."
def process_with_rag(self, user_input, rag_documents):
"""RAG ๋ฌธ์„œ์™€ ํ•จ๊ป˜ ์ฒ˜๋ฆฌ"""
# RAG ๋ฌธ์„œ๋ฅผ ์ปจํ…์ŠคํŠธ๋กœ ๋ณ€ํ™˜
if rag_documents and len(rag_documents) > 0:
rag_context = "\n\n".join([doc.get('content', '') for doc in rag_documents])
else:
rag_context = ""
return self.generate_ai_response(user_input, rag_context)
# ์ „์—ญ ์ธ์Šคํ„ด์Šค
_llm_processor = None
def get_llm_processor():
"""LLM ํ”„๋กœ์„ธ์„œ ์‹ฑ๊ธ€ํ„ด ์ธ์Šคํ„ด์Šค ๋ฐ˜ํ™˜"""
global _llm_processor
if _llm_processor is None:
_llm_processor = LLMProcessor()
return _llm_processor
def is_llm_available():
"""LLM ์‹œ์Šคํ…œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ํ™•์ธ"""
return TRANSFORMERS_AVAILABLE and torch.cuda.is_available()
def process_with_llm(user_input, rag_documents=None):
"""ํŽธ์˜ ํ•จ์ˆ˜: RAG ๊ฒฐ๊ณผ์™€ ํ•จ๊ป˜ LLM ์ฒ˜๋ฆฌ"""
processor = get_llm_processor()
if rag_documents:
return processor.process_with_rag(user_input, rag_documents)
else:
return processor.generate_ai_response(user_input)
if __name__ == "__main__":
# ํ…Œ์ŠคํŠธ ์ฝ”๋“œ
print("๐Ÿงช LLM ํ”„๋กœ์„ธ์„œ ํ…Œ์ŠคํŠธ")
processor = LLMProcessor()
# ์ดˆ๊ธฐํ™” ํ…Œ์ŠคํŠธ
if processor.initialize_model(force_cpu=True):
print("โœ… ๋ชจ๋ธ ์ดˆ๊ธฐํ™” ์„ฑ๊ณต")
# ๊ฐ„๋‹จํ•œ ํ…Œ์ŠคํŠธ
test_input = "๊ฐ•๋‚จ๊ตฌ 10์–ต์› ์•„ํŒŒํŠธ 3์ฃผํƒ์ž ์ทจ๋“์„ธ"
response = processor.generate_ai_response(test_input)
print(f"์‘๋‹ต: {response[:100]}...")
else:
print("โŒ ๋ชจ๋ธ ์ดˆ๊ธฐํ™” ์‹คํŒจ")