compositeAI / app.py
jeongkee's picture
Upload 5 files
e4fcc0b verified
# =========================================================
# POSCO DX - MRO Composite AI - PROCESS GUIDE ENHANCED
# ์—…๋ฌด ํ”„๋กœ์„ธ์Šค ๊ฐ€์ด๋“œ ํ†ตํ•ฉ ๋ฒ„์ „ - Hugging Face Spaces ๋ฐฐํฌ์šฉ
# =========================================================
import os
import json
import time
import random
import traceback
from dataclasses import dataclass
from typing import Dict, Any, List, Optional, Tuple, TypedDict
from datetime import datetime, timedelta
import numpy as np
import pandas as pd
import networkx as nx
# โœ… Plotly imports
import plotly
import plotly.graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots
print(f"โœ… NumPy: {np.__version__}")
print(f"โœ… Pandas: {pd.__version__}")
print(f"โœ… Plotly: {plotly.__version__}")
try:
from pulp import LpProblem, LpMinimize, LpVariable, lpSum, LpStatus
PULP_AVAILABLE = True
print("โœ… PuLP available")
except ImportError:
print("โš ๏ธ PuLP not available")
PULP_AVAILABLE = False
import gradio as gr
print(f"โœ… Gradio: {gr.__version__}")
try:
from langgraph.graph import StateGraph, END
LANGGRAPH_AVAILABLE = True
print("โœ… LangGraph available")
except ImportError:
print("โš ๏ธ LangGraph not available")
LANGGRAPH_AVAILABLE = False
try:
from openai import OpenAI
OPENAI_AVAILABLE = True
print("โœ… OpenAI available")
except ImportError:
print("โš ๏ธ OpenAI not available")
OPENAI_AVAILABLE = False
# =========================================================
# API Key Configuration for Hugging Face Spaces
# =========================================================
# Hugging Face Spaces์—์„œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ API ํ‚ค ๋กœ๋“œ
OPENAI_API_KEY = os.environ.get('OPENAI_API_KEY', '').strip()
if OPENAI_API_KEY:
os.environ['OPENAI_API_KEY'] = OPENAI_API_KEY
print("โœ… OpenAI API Key loaded from environment")
else:
print("โš ๏ธ DEMO MODE - No API Key found")
print("๐Ÿ’ก To use OpenAI features, add OPENAI_API_KEY to your Hugging Face Space Secrets")
print("\n" + "=" * 60)
print("โœ… ํ”„๋กœ์„ธ์Šค ๊ฐ€์ด๋“œ ํ†ตํ•ฉ ๋ฒ„์ „ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ!")
print("=" * 60 + "\n")
# =========================================================
# Process Guide Configuration
# =========================================================
PROCESS_WORKFLOWS = {
"mro": {
"title": "๐Ÿ”ง MRO ์šด์˜ ํ”„๋กœ์„ธ์Šค",
"steps": [
{
"id": "1",
"name": "๊ณ ์žฅ/์ •๋น„ ์š”์ฒญ ์ ‘์ˆ˜",
"description": "์„ค๋น„ ๊ณ ์žฅ ๋˜๋Š” ์˜ˆ๋ฐฉ์ •๋น„ ์š”์ฒญ์„ ์ ‘์ˆ˜ํ•ฉ๋‹ˆ๋‹ค",
"input": "์„ค๋น„ ID, ๊ณ ์žฅ ์œ ํ˜•, ์šฐ์„ ์ˆœ์œ„",
"output": "์š”์ฒญ ๋ฒˆํ˜ธ, ์„ค๋น„ ์ƒ์„ธ์ •๋ณด",
"owner": "ํ˜„์žฅ ๋‹ด๋‹น์ž โ†’ MROํŒ€",
"duration": "5๋ถ„"
},
{
"id": "2",
"name": "์„ค๋น„ ์ •๋ณด ์กฐํšŒ",
"description": "Knowledge Graph์—์„œ ์„ค๋น„ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค",
"input": "์„ค๋น„ ID",
"output": "์„ค๋น„๋ช…, ์œ„์น˜, ์ค‘์š”๋„, ์ •๋น„์ด๋ ฅ",
"owner": "MROํŒ€ (AI ์ž๋™)",
"duration": "1๋ถ„"
},
{
"id": "3",
"name": "ํ˜ธํ™˜ ๋ถ€ํ’ˆ ์ž๋™ ๋งค์นญ",
"description": "์„ค๋น„์™€ ํ˜ธํ™˜๋˜๋Š” ๋ชจ๋“  ๋ถ€ํ’ˆ์„ ์ž๋™์œผ๋กœ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค",
"input": "์„ค๋น„ ID, ์„ค๋น„ ํƒ€์ž…",
"output": "ํ˜ธํ™˜ ๋ถ€ํ’ˆ ๋ฆฌ์ŠคํŠธ, ํ•„์ˆ˜/์„ ํƒ ๊ตฌ๋ถ„",
"owner": "MROํŒ€ (AI ์ž๋™)",
"duration": "2๋ถ„"
},
{
"id": "4",
"name": "์ „์‚ฌ ์žฌ๊ณ  ํ˜„ํ™ฉ ํ™•์ธ",
"description": "๋ณธ์‚ฌ ๋ฐ ๊ฐ ์ œ์ฒ ์†Œ์˜ ์žฌ๊ณ  ํ˜„ํ™ฉ์„ ์‹ค์‹œ๊ฐ„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค",
"input": "ํ’ˆ๋ชฉ ID",
"output": "์ฐฝ๊ณ ๋ณ„ ์žฌ๊ณ ๋Ÿ‰, ์•ˆ์ „์žฌ๊ณ , ์˜ˆ์•ฝ์ˆ˜๋Ÿ‰",
"owner": "MROํŒ€ (AI ์ž๋™)",
"duration": "1๋ถ„"
},
{
"id": "5",
"name": "๋ฐœ์ฃผ ํ•„์š”์„ฑ ํŒ๋‹จ",
"description": "์žฌ๊ณ  ๋ถ€์กฑ ์‹œ ๋ฐœ์ฃผ ์š”์ฒญ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค",
"input": "ํ˜„์žฌ๊ณ , ์•ˆ์ „์žฌ๊ณ , ์ˆ˜์š”๋Ÿ‰",
"output": "๋ฐœ์ฃผ ํ•„์š” ์—ฌ๋ถ€, ๋ฐœ์ฃผ ์ˆ˜๋Ÿ‰",
"owner": "MROํŒ€",
"duration": "3๋ถ„"
},
{
"id": "6",
"name": "๊ตฌ๋งคํŒ€ ๋ฐœ์ฃผ ์š”์ฒญ",
"description": "๊ตฌ๋งคํŒ€์— ๋ฐœ์ฃผ ์š”์ฒญ์„œ๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค",
"input": "ํ’ˆ๋ชฉ ์ •๋ณด, ์ˆ˜๋Ÿ‰, ๋‚ฉ๊ธฐ ์š”๊ตฌ์‚ฌํ•ญ",
"output": "๋ฐœ์ฃผ ์š”์ฒญ ๋ฒˆํ˜ธ",
"owner": "MROํŒ€ โ†’ ๊ตฌ๋งคํŒ€",
"duration": "2๋ถ„"
}
],
"total_duration": "์•ฝ 15๋ถ„",
"success_criteria": [
"โœ“ ์„ค๋น„ ์ •๋ณด ์ •ํ™•ํžˆ ์‹๋ณ„",
"โœ“ ํ˜ธํ™˜ ๋ถ€ํ’ˆ 100% ๋งค์นญ",
"โœ“ ์žฌ๊ณ  ํ˜„ํ™ฉ ์‹ค์‹œ๊ฐ„ ๋ฐ˜์˜",
"โœ“ ๋ฐœ์ฃผ ์ˆ˜๋Ÿ‰ ์ตœ์ ํ™”"
]
},
"procurement": {
"title": "๐Ÿ’ฐ ๊ตฌ๋งค/์กฐ๋‹ฌ ํ”„๋กœ์„ธ์Šค",
"steps": [
{
"id": "1",
"name": "๋ฐœ์ฃผ ์š”์ฒญ ์ ‘์ˆ˜",
"description": "MROํŒ€์œผ๋กœ๋ถ€ํ„ฐ ๋ฐœ์ฃผ ์š”์ฒญ์„ ์ ‘์ˆ˜ํ•ฉ๋‹ˆ๋‹ค",
"input": "๋ฐœ์ฃผ ์š”์ฒญ์„œ, ํ’ˆ๋ชฉ, ์ˆ˜๋Ÿ‰, ๋‚ฉ๊ธฐ",
"output": "๊ตฌ๋งค ์ž‘์—… ๋ฒˆํ˜ธ",
"owner": "๊ตฌ๋งคํŒ€",
"duration": "3๋ถ„"
},
{
"id": "2",
"name": "๊ณต๊ธ‰์—…์ฒด ์ •๋ณด ์กฐํšŒ",
"description": "ํ’ˆ๋ชฉ๋ณ„ ๋“ฑ๋ก๋œ ๋ชจ๋“  ๊ณต๊ธ‰์—…์ฒด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค",
"input": "ํ’ˆ๋ชฉ ID",
"output": "๊ณต๊ธ‰์—…์ฒด ๋ฆฌ์ŠคํŠธ, ๋‹จ๊ฐ€, ๋‚ฉ๊ธฐ, ESG๋“ฑ๊ธ‰",
"owner": "๊ตฌ๋งคํŒ€ (AI ์ž๋™)",
"duration": "2๋ถ„"
},
{
"id": "3",
"name": "๊ทœ์ • ์ค€์ˆ˜ ๊ฒ€์ฆ",
"description": "Neuro-Symbolic AI๋กœ ๊ตฌ๋งค ๊ทœ์ •์„ ์ž๋™ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค",
"input": "ํ’ˆ๋ชฉ ์†์„ฑ, ๊ณต๊ธ‰์—…์ฒด ์ •๋ณด",
"output": "๊ทœ์ • ์œ„๋ฐ˜ ์—ฌ๋ถ€, ์ฐจ๋‹จ/๊ฒฝ๊ณ  ๋ฆฌ์ŠคํŠธ",
"owner": "๊ตฌ๋งคํŒ€ (AI ์ž๋™)",
"duration": "1๋ถ„"
},
{
"id": "4",
"name": "์ตœ์  ๋ฐฐ๋ถ„ ๊ณ„์‚ฐ",
"description": "Linear Programming์œผ๋กœ ์ตœ์  ๋ฐœ์ฃผ ๊ณ„ํš์„ ์ˆ˜๋ฆฝํ•ฉ๋‹ˆ๋‹ค",
"input": "๊ณต๊ธ‰์—…์ฒด ์˜คํผ, ์ˆ˜์š”๋Ÿ‰, ์ œ์•ฝ์กฐ๊ฑด",
"output": "์—…์ฒด๋ณ„ ๋ฐœ์ฃผ๋Ÿ‰, ์ด ๋น„์šฉ, ์˜ˆ์ƒ ๋‚ฉ๊ธฐ",
"owner": "๊ตฌ๋งคํŒ€ (AI ์ž๋™)",
"duration": "2๋ถ„"
},
{
"id": "5",
"name": "๋ฐœ์ฃผ ์ „๋žต ์ˆ˜๋ฆฝ",
"description": "LLM์ด ์ตœ์ ํ™” ๊ฒฐ๊ณผ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๊ตฌ๋งค ์ „๋žต์„ ์ œ์•ˆํ•ฉ๋‹ˆ๋‹ค",
"input": "์ตœ์ ํ™” ๊ฒฐ๊ณผ, ์‹œ์žฅ ์ƒํ™ฉ",
"output": "๋ฐœ์ฃผ ์ „๋žต, ๋ฆฌ์Šคํฌ ๋ถ„์„, ๋Œ€์•ˆ",
"owner": "๊ตฌ๋งคํŒ€ (AI ์ง€์›)",
"duration": "5๋ถ„"
},
{
"id": "6",
"name": "๊ฒฝ์˜์ง„ ์Šน์ธ ์š”์ฒญ",
"description": "๋ฐœ์ฃผ ๊ณ„ํš์„ ๊ฒฝ์˜์ง„์—๊ฒŒ ์Šน์ธ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค",
"input": "๋ฐœ์ฃผ ๊ณ„ํš์„œ, ๋น„์šฉ ๋ถ„์„",
"output": "์Šน์ธ ์š”์ฒญ ๋ฒˆํ˜ธ",
"owner": "๊ตฌ๋งคํŒ€ โ†’ ๊ฒฝ์˜์ง„",
"duration": "3๋ถ„"
},
{
"id": "7",
"name": "PO ๋ฐœํ–‰ (์Šน์ธ ํ›„)",
"description": "์Šน์ธ ํ›„ ๊ณต๊ธ‰์—…์ฒด์— ์ •์‹ ๋ฐœ์ฃผ์„œ๋ฅผ ๋ฐœํ–‰ํ•ฉ๋‹ˆ๋‹ค",
"input": "์Šน์ธ๋œ ๋ฐœ์ฃผ ๊ณ„ํš",
"output": "PO ๋ฒˆํ˜ธ, ๊ณ„์•ฝ์„œ",
"owner": "๊ตฌ๋งคํŒ€",
"duration": "10๋ถ„"
}
],
"total_duration": "์•ฝ 25๋ถ„ (์Šน์ธ ๋Œ€๊ธฐ ์ œ์™ธ)",
"success_criteria": [
"โœ“ ๊ทœ์ • 100% ์ค€์ˆ˜",
"โœ“ ๋น„์šฉ ์ตœ์ ํ™” ๋‹ฌ์„ฑ",
"โœ“ ๋‚ฉ๊ธฐ ์š”๊ตฌ์‚ฌํ•ญ ์ถฉ์กฑ",
"โœ“ ESG ๋“ฑ๊ธ‰ ๊ธฐ์ค€ ๋งŒ์กฑ"
]
},
"executive": {
"title": "๐Ÿ‘” ๊ฒฝ์˜์ง„ ์˜์‚ฌ๊ฒฐ์ • ํ”„๋กœ์„ธ์Šค",
"steps": [
{
"id": "1",
"name": "์Šน์ธ ์š”์ฒญ ์•Œ๋ฆผ",
"description": "๋ฐœ์ฃผ ์Šน์ธ ์š”์ฒญ ์•Œ๋ฆผ์„ ์ˆ˜์‹ ํ•ฉ๋‹ˆ๋‹ค",
"input": "์Šน์ธ ์š”์ฒญ ๋ฒˆํ˜ธ, ์š”์•ฝ ์ •๋ณด",
"output": "์•Œ๋ฆผ ํ™•์ธ",
"owner": "์‹œ์Šคํ…œ โ†’ ๊ฒฝ์˜์ง„",
"duration": "์ฆ‰์‹œ"
},
{
"id": "2",
"name": "KPI ๋Œ€์‹œ๋ณด๋“œ ํ™•์ธ",
"description": "์‹ค์‹œ๊ฐ„ KPI ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ํ†ตํ•ด ์ „๋ฐ˜์  ํ˜„ํ™ฉ์„ ํŒŒ์•…ํ•ฉ๋‹ˆ๋‹ค",
"input": "์—†์Œ",
"output": "๋น„์šฉ์ ˆ๊ฐ๋ฅ , ์ปดํ”Œ๋ผ์ด์–ธ์Šค, ESG์ ์ˆ˜ ๋“ฑ",
"owner": "๊ฒฝ์˜์ง„",
"duration": "2๋ถ„"
},
{
"id": "3",
"name": "Action Items ๊ฒ€ํ† ",
"description": "์šฐ์„ ์ˆœ์œ„๋ณ„ ์กฐ์น˜ ํ•ญ๋ชฉ์„ ๊ฒ€ํ† ํ•ฉ๋‹ˆ๋‹ค",
"input": "Action Items ๋ฆฌ์ŠคํŠธ",
"output": "๊ฒ€ํ†  ์˜๊ฒฌ",
"owner": "๊ฒฝ์˜์ง„",
"duration": "5๋ถ„"
},
{
"id": "4",
"name": "๋ฐœ์ฃผ ์ƒ์„ธ ๋ถ„์„",
"description": "๋ฐœ์ฃผ ๊ณ„ํš์˜ ํƒ€๋‹น์„ฑ์„ ๋ฉด๋ฐ€ํžˆ ๊ฒ€ํ† ํ•ฉ๋‹ˆ๋‹ค",
"input": "๋ฐœ์ฃผ ๊ณ„ํš์„œ, ์ตœ์ ํ™” ๊ฒฐ๊ณผ, ๊ทœ์ • ๊ฒ€์ฆ",
"output": "๋ถ„์„ ์˜๊ฒฌ",
"owner": "๊ฒฝ์˜์ง„",
"duration": "10๋ถ„"
},
{
"id": "5",
"name": "์˜์‚ฌ๊ฒฐ์ •",
"description": "์Šน์ธ/๋ฐ˜๋ ค/์กฐ๊ฑด๋ถ€์Šน์ธ์„ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค",
"input": "๊ฒ€ํ†  ๊ฒฐ๊ณผ",
"output": "์Šน์ธ ๊ฒฐ์ •, ํ”ผ๋“œ๋ฐฑ",
"owner": "๊ฒฝ์˜์ง„",
"duration": "3๋ถ„"
},
{
"id": "6",
"name": "ํ”ผ๋“œ๋ฐฑ ์ œ๊ณต",
"description": "๊ฐœ์„  ์ œ์•ˆ ๋˜๋Š” ์ง€์‹œ์‚ฌํ•ญ์„ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค",
"input": "์˜์‚ฌ๊ฒฐ์ • ๊ทผ๊ฑฐ",
"output": "ํ”ผ๋“œ๋ฐฑ ๋ฉ”์‹œ์ง€, ๊ฐœ์„  ๋ฐฉํ–ฅ",
"owner": "๊ฒฝ์˜์ง„ โ†’ ๊ตฌ๋งคํŒ€",
"duration": "5๋ถ„"
}
],
"total_duration": "์•ฝ 25๋ถ„",
"success_criteria": [
"โœ“ ์ „๋žต์  ํƒ€๋‹น์„ฑ ๊ฒ€์ฆ",
"โœ“ ๋ฆฌ์Šคํฌ ์ˆ˜์šฉ ๊ฐ€๋Šฅ ์ˆ˜์ค€",
"โœ“ ์˜ˆ์‚ฐ ๋ฒ”์œ„ ๋‚ด ์ง‘ํ–‰",
"โœ“ ์žฅ๊ธฐ ๋ชฉํ‘œ ๋ถ€ํ•ฉ"
]
}
}
# =========================================================
# Enhanced Configuration with Real Part Names
# =========================================================
SCENARIO_PRESETS = {
"๊ธด๊ธ‰ ๊ณ ์žฅ ๋Œ€์‘": {
"description": "๐Ÿšจ ํฌํ•ญ์ œ์ฒ ์†Œ ์ปจ๋ฒ ์ด์–ด ๋ฒ ์–ด๋ง ๊ธด๊ธ‰ ๊ณ ์žฅ",
"equipment_id": "CONV-PH-007",
"item_id": "",
"demand_qty": 10,
"context": "์ปจ๋ฒ ์ด์–ด ๋ฒ ์–ด๋ง ๊ณ ์žฅ์œผ๋กœ ์ƒ์‚ฐ๋ผ์ธ ์ค‘๋‹จ. ์ฆ‰์‹œ ๊ต์ฒด ํ•„์š”.",
"priority": "๊ธด๊ธ‰",
"guide": "๋ฆฌ๋“œํƒ€์ž„ ์ตœ์†Œํ™” ์šฐ์„ . ๊ตญ๋‚ด ๊ณต๊ธ‰์—…์ฒด ์šฐ์„  ๊ณ ๋ ค."
},
"์ •๊ธฐ ๋ฐœ์ฃผ ๊ณ„ํš": {
"description": "๐Ÿ“‹ ์›”๊ฐ„ ์ •๊ธฐ ๋ฐœ์ฃผ - ์œ ์••ํŽŒํ”„ ์˜ˆ๋ฐฉ์ •๋น„",
"equipment_id": "PUMP-GY-003",
"item_id": "SEAL-A45",
"demand_qty": 50,
"context": "์›”๊ฐ„ ์˜ˆ๋ฐฉ์ •๋น„ ๊ณ„ํš. ์ตœ์  ๊ฐ€๊ฒฉ ๋ฐ ์žฌ๊ณ  ๊ท ํ˜• ํ•„์š”.",
"priority": "์ •์ƒ",
"guide": "๋น„์šฉ ์ตœ์ ํ™” ์šฐ์„ . ESG ๋“ฑ๊ธ‰ ๊ณ ๋ ค."
},
"๊ทœ์ • ์ค€์ˆ˜ ๊ฒ€์ฆ": {
"description": "โš–๏ธ ๊ทœ์ œํ’ˆ๋ชฉ(ํŠน์ˆ˜ํ™”ํ•™๋ฌผ์งˆ) ๊ตฌ๋งค ๊ฒ€์ฆ",
"equipment_id": "VALVE-PH-005",
"item_id": "",
"demand_qty": 20,
"context": "ํŠน์ˆ˜ ์‹ค๋ง์žฌ ๊ตฌ๋งค. ํ•ด์™ธ๊ตฌ๋งค ์ฐจ๋‹จ ๊ทœ์ • ์ค€์ˆ˜ ํ•„์ˆ˜.",
"priority": "๊ทœ์ •์ค€์ˆ˜",
"guide": "์ปดํ”Œ๋ผ์ด์–ธ์Šค 100% ์ค€์ˆ˜. ๊ตญ๋‚ด์—…์ฒด๋งŒ ํ—ˆ์šฉ."
}
}
# Real part names and categories
REAL_PART_NAMES = {
"๋ฒ ์–ด๋ง": ["SKF 6205 ๋ณผ๋ฒ ์–ด๋ง", "NSK ์›ํ†ต๋ฒ ์–ด๋ง", "NTN ํ…Œ์ดํผ๋ฒ ์–ด๋ง"],
"์œคํ™œ์œ ": ["์‰˜ ์˜ค๋งˆ๋ผ 220", "๋ชจ๋นŒ DTE 25", "์ง€์—์Šค์นผํ…์Šค ํ„ฐ๋นˆ์œ "],
"ํ•„ํ„ฐ": ["ํ•˜์ด๋“œ๋กœ๋ฝ ์œ ์••ํ•„ํ„ฐ", "ํŒŒ์ปค ์—์–ดํ•„ํ„ฐ", "๋„๋‚œ๋“œ์Šจ ์ •๋ฐ€ํ•„ํ„ฐ"],
"๋ฒจํŠธ": ["๊ฒŒ์ด์ธ  ํŒŒ์›Œ๊ทธ๋ฆฝ ๋ฒจํŠธ", "๋ฐ˜๋„ V๋ฒจํŠธ", "์˜ตํ‹ฐ๋ฒจํŠธ ํƒ€์ด๋ฐ๋ฒจํŠธ"],
"์„ผ์„œ": ["์ง€๋ฉ˜์Šค ๊ทผ์ ‘์„ผ์„œ", "์˜ค๋ฏ€๋ก  ๊ด‘์ „์„ผ์„œ", "ํ•˜๋‹ˆ์›ฐ ์••๋ ฅ์„ผ์„œ"],
"ํŒจํ‚น": ["NOK ์˜ค๋ง", "ํŒŒ์ปค ์œ ์••์”ฐ", "๋ฐœ์นด ๊ทธ๋žœ๋“œํŒจํ‚น"],
"ํ“จ์ฆˆ": ["LS์‚ฐ์ „ MCCB", "์Šˆ๋‚˜์ด๋” ์ฐจ๋‹จ๊ธฐ", "ABB ํ“จ์ฆˆ"],
"ํ˜ธ์Šค": ["ํŒŒ์ปค ์œ ์••ํ˜ธ์Šค", "๋งŒ๋ฆฌ ๊ณ ์••ํ˜ธ์Šค", "๋ธŒ๋ฆฌ์ง€์Šคํ†ค ์‚ฐ์—…ํ˜ธ์Šค"],
"๋ณผํŠธ": ["SUS304 ์œก๊ฐ๋ณผํŠธ", "๊ณ ์žฅ๋ ฅ๋ณผํŠธ F10T", "์•ต์ปค๋ณผํŠธ M16"],
"์‹ค๋ง์žฌ": ["๋กํƒ€์ดํŠธ ์‹ค๋ž€ํŠธ", "์“ฐ๋ฆฌ๋ณธ๋“œ ์•ก์ƒํŒจํ‚น", "ํ—จ์ผˆ ๋ฐ€๋ด‰์žฌ"]
}
# Enhanced supplier info
REAL_SUPPLIERS = [
{"name": "ํฌ์Šค์ฝ”์ผ€๋ฏธ์นผ", "type": "๊ตญ๋‚ด", "esg": "A", "specialty": "ํ™”ํ•™/์œคํ™œ์œ "},
{"name": "ํšจ์„ฑ์ค‘๊ณต์—…", "type": "๊ตญ๋‚ด", "esg": "A", "specialty": "๋ฒ ์–ด๋ง/๊ธฐ๊ณ„"},
{"name": "LS์‚ฐ์ „", "type": "๊ตญ๋‚ด", "esg": "B", "specialty": "์ „๊ธฐ/์„ผ์„œ"},
{"name": "์‚ผํ™”์ฝ˜๋ด์„œ", "type": "๊ตญ๋‚ด", "esg": "B", "specialty": "์ „๊ธฐ๋ถ€ํ’ˆ"},
{"name": "ํƒœ๊ด‘์‚ฐ์—…", "type": "๊ตญ๋‚ด", "esg": "C", "specialty": "ํ˜ธ์Šค/ํŒจํ‚น"},
{"name": "ํ•œ๊ตญํŒŒ์ปค", "type": "๊ตญ๋‚ด", "esg": "A", "specialty": "์œ ์••๋ถ€ํ’ˆ"},
{"name": "๊ทธ๋ผ์ฝ”(Graco)", "type": "ํ•ด์™ธ", "esg": "B", "specialty": "์œ ์••์žฅ๋น„"},
{"name": "์—๋จธ์Šจ(Emerson)", "type": "ํ•ด์™ธ", "esg": "C", "specialty": "๋ฐธ๋ธŒ/์„ผ์„œ"}
]
# =========================================================
# Utility Functions
# =========================================================
def now_ts() -> str:
return time.strftime("%Y-%m-%d %H:%M:%S")
def safe_json(obj: Any) -> str:
try:
return json.dumps(obj, ensure_ascii=False, indent=2)
except Exception:
return str(obj)
def format_status(status_dict: Dict[str, Any]) -> str:
lines = [
"=" * 60,
"๐Ÿ“Š ์‹œ์Šคํ…œ ์‹คํ–‰ ์ƒํƒœ",
"=" * 60,
"",
f"๐Ÿ”Œ ์—ฐ๊ฒฐ: {status_dict.get('mode', 'Unknown')}",
f"๐ŸŽฏ ์‹œ๋‚˜๋ฆฌ์˜ค: {status_dict.get('scenario', 'N/A')}",
f"โš™๏ธ ์„ค๋น„: {status_dict.get('equipment', 'N/A')}",
f"๐Ÿ“ฆ ํ’ˆ๋ชฉ: {status_dict.get('item_name', 'N/A')}",
f"๐Ÿ“Š ์ˆ˜์š”: {status_dict.get('demand', 'N/A')}๊ฐœ",
f"๐Ÿšจ ์šฐ์„ ์ˆœ์œ„: {status_dict.get('priority', 'N/A')}",
f"\nโœ… ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ: {'ํ†ต๊ณผ' if status_dict.get('tables_ok') else '์‹คํŒจ'}",
f"โฑ๏ธ ์ง„ํ–‰: {status_dict.get('progress', 'N/A')}",
"\n" + "=" * 60
]
return "\n".join(lines)
def create_process_guide_html(process_key: str) -> str:
"""์—…๋ฌด ํ”„๋กœ์„ธ์Šค ๊ฐ€์ด๋“œ๋ฅผ HTML๋กœ ์ƒ์„ฑ"""
workflow = PROCESS_WORKFLOWS.get(process_key, {})
if not workflow:
return "<p>ํ”„๋กœ์„ธ์Šค ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</p>"
html = f"""
<div style="font-family: 'Malgun Gothic', Arial, sans-serif; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 10px; color: white;">
<h2 style="margin-top: 0;">{workflow['title']}</h2>
<p style="font-size: 14px; opacity: 0.9;">์ด ์†Œ์š”์‹œ๊ฐ„: <strong>{workflow['total_duration']}</strong></p>
</div>
<div style="margin-top: 20px;">
"""
for step in workflow['steps']:
html += f"""
<div style="margin-bottom: 20px; padding: 15px; border-left: 4px solid #667eea; background: #f8f9fa; border-radius: 5px;">
<div style="display: flex; align-items: center; margin-bottom: 10px;">
<div style="background: #667eea; color: white; width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; margin-right: 10px;">
{step['id']}
</div>
<h3 style="margin: 0; color: #2c3e50;">{step['name']}</h3>
<span style="margin-left: auto; background: #e3f2fd; padding: 3px 10px; border-radius: 10px; font-size: 12px; color: #1976d2;">
โฑ๏ธ {step['duration']}
</span>
</div>
<p style="margin: 10px 0; color: #555;">{step['description']}</p>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px;">
<div style="background: white; padding: 10px; border-radius: 5px; border: 1px solid #e0e0e0;">
<strong style="color: #1976d2;">๐Ÿ“ฅ ์ž…๋ ฅ:</strong><br>
<span style="font-size: 13px; color: #666;">{step['input']}</span>
</div>
<div style="background: white; padding: 10px; border-radius: 5px; border: 1px solid #e0e0e0;">
<strong style="color: #388e3c;">๐Ÿ“ค ์ถœ๋ ฅ:</strong><br>
<span style="font-size: 13px; color: #666;">{step['output']}</span>
</div>
</div>
<div style="margin-top: 10px; padding: 8px; background: white; border-radius: 5px; border: 1px solid #e0e0e0;">
<strong style="color: #f57c00;">๐Ÿ‘ค ๋‹ด๋‹น:</strong>
<span style="font-size: 13px; color: #666;">{step['owner']}</span>
</div>
</div>
"""
html += """
</div>
<div style="margin-top: 30px; padding: 20px; background: #e8f5e9; border-radius: 10px; border-left: 4px solid #4caf50;">
<h3 style="margin-top: 0; color: #2e7d32;">โœ… ์„ฑ๊ณต ๊ธฐ์ค€</h3>
<ul style="margin: 0; padding-left: 20px;">
"""
for criterion in workflow['success_criteria']:
html += f"<li style='margin: 5px 0; color: #1b5e20;'>{criterion}</li>"
html += """
</ul>
</div>
"""
return html
# =========================================================
# Enhanced Data Generator with Real Names
# =========================================================
def generate_demo_tables(seed: int = 7) -> Dict[str, pd.DataFrame]:
"""Generate realistic demo data"""
random.seed(seed)
np.random.seed(seed)
plants = pd.DataFrame([
{"plant_id": "PH", "plant_name": "ํฌํ•ญ์ œ์ฒ ์†Œ", "region": "๊ฒฝ๋ถ", "capacity": 1000},
{"plant_id": "GY", "plant_name": "๊ด‘์–‘์ œ์ฒ ์†Œ", "region": "์ „๋‚จ", "capacity": 1200},
{"plant_id": "HQ", "plant_name": "๋ณธ์‚ฌ", "region": "์„œ์šธ", "capacity": 0},
])
# Equipment with real names
equipment = []
eq_configs = [
("PUMP", "์œ ์••ํŽŒํ”„", ["PH", "GY"], 6),
("CONV", "์ปจ๋ฒ ์ด์–ด", ["PH", "GY"], 4),
("VALVE", "์ œ์–ด๋ฐธ๋ธŒ", ["PH", "GY"], 3),
("MOTOR", "๊ตฌ๋™๋ชจํ„ฐ", ["PH", "GY"], 5),
]
eq_id = 1
for eq_type, eq_name_kr, plants_list, count in eq_configs:
for plant in plants_list:
for i in range(1, count + 1):
equipment.append({
"equipment_id": f"{eq_type}-{plant}-{eq_id:03d}",
"equipment_name": f"{eq_name_kr}-{plant}-{i}ํ˜ธ๊ธฐ",
"plant_id": plant,
"equipment_type": eq_name_kr,
"criticality": random.choice(["๊ธด๊ธ‰", "๊ธด๊ธ‰", "์ค‘์š”", "๋ณดํ†ต"]),
"status": "๊ฐ€๋™์ค‘",
"last_maintenance": (datetime.now() - timedelta(days=random.randint(30, 180))).strftime("%Y-%m-%d"),
})
eq_id += 1
equipment = pd.DataFrame(equipment)
# Items with real part names
items = []
item_id = 1
for category, part_list in REAL_PART_NAMES.items():
for part_name in part_list:
items.append({
"item_id": f"{category[:3].upper()}-{chr(65 + (item_id % 3))}{item_id:02d}",
"item_name": part_name,
"category": category,
"uom": "EA",
"risk_class": "๊ทœ์ œ" if "ํ™”ํ•™" in part_name or "ํŠน์ˆ˜" in category else "์ผ๋ฐ˜",
"unit_weight": round(0.5 + random.random() * 5, 1),
"shelf_life_days": random.choice([365, 730, 1095, None]),
})
item_id += 1
items = pd.DataFrame(items)
# Compatibility
compat = []
for eq_idx, eq_row in equipment.iterrows():
eq_id = eq_row["equipment_id"]
eq_type = eq_row["equipment_type"]
# Match parts to equipment type
if "ํŽŒํ”„" in eq_type:
relevant_cats = ["๋ฒ ์–ด๋ง", "์œคํ™œ์œ ", "ํŒจํ‚น"]
elif "์ปจ๋ฒ ์ด์–ด" in eq_type:
relevant_cats = ["๋ฒ ์–ด๋ง", "๋ฒจํŠธ", "์„ผ์„œ"]
elif "๋ฐธ๋ธŒ" in eq_type:
relevant_cats = ["์‹ค๋ง์žฌ", "ํŒจํ‚น", "์œคํ™œ์œ "]
else:
relevant_cats = ["๋ฒ ์–ด๋ง", "์„ผ์„œ", "ํ•„ํ„ฐ"]
for cat in relevant_cats:
cat_items = items[items["category"] == cat]
if len(cat_items) > 0:
selected = cat_items.sample(min(2, len(cat_items)))
for _, item in selected.iterrows():
compat.append({
"equipment_id": eq_id,
"item_id": item["item_id"],
"is_mandatory": (cat == relevant_cats[0]),
"annual_consumption_est": random.randint(20, 200),
"failure_rate": round(random.random() * 0.05, 3),
})
compat = pd.DataFrame(compat).drop_duplicates(["equipment_id", "item_id"])
# Storages
storages = pd.DataFrame([
{"storage_id": "WH-HQ", "plant_id": "HQ", "storage_name": "๋ณธ์‚ฌ ์ค‘์•™์ฐฝ๊ณ ", "capacity": 10000},
{"storage_id": "WH-PH", "plant_id": "PH", "storage_name": "ํฌํ•ญ MRO์ฐฝ๊ณ ", "capacity": 5000},
{"storage_id": "WH-GY", "plant_id": "GY", "storage_name": "๊ด‘์–‘ MRO์ฐฝ๊ณ ", "capacity": 5000},
])
# Inventory with realistic levels
inventory = []
for st_idx, st_row in storages.iterrows():
sampled_items = items.sample(min(25, len(items)))
for _, item in sampled_items.iterrows():
stock_level = random.randint(10, 100)
safety_stock = int(stock_level * 0.2)
inventory.append({
"storage_id": st_row["storage_id"],
"item_id": item["item_id"],
"on_hand": stock_level,
"safety_stock": safety_stock,
"reserved": random.randint(0, min(5, stock_level)),
"last_updated": (datetime.now() - timedelta(days=random.randint(1, 30))).strftime("%Y-%m-%d"),
})
inventory = pd.DataFrame(inventory)
# Suppliers with real names
suppliers = pd.DataFrame([
{
"supplier_id": f"SUP-{i:03d}",
"supplier_name": sup["name"],
"supplier_type": sup["type"],
"rating": round(3.5 + random.random() * 1.5, 1),
"esg_level": sup["esg"],
"specialty": sup["specialty"],
"region": sup["type"],
"payment_terms": random.choice(["NET30", "NET45", "NET60"]),
"established_year": random.randint(1990, 2020),
}
for i, sup in enumerate(REAL_SUPPLIERS, 1)
])
# Supplier offers with realistic pricing
offers = []
for _, item in items.iterrows():
num_suppliers = random.randint(3, 4)
selected_sups = suppliers.sample(min(num_suppliers, len(suppliers)))
base_price = 10000 + random.randint(0, 90000)
for rank, (_, sup) in enumerate(selected_sups.iterrows()):
price_multiplier = 1.0 + (rank * 0.05) + random.uniform(-0.1, 0.1)
offers.append({
"item_id": item["item_id"],
"supplier_id": sup["supplier_id"],
"unit_price": int(base_price * price_multiplier),
"lead_time_days": 3 + rank * 2 + random.randint(0, 5),
"moq": [10, 20, 50, 100][rank % 4],
"contract_type": random.choice(["๋‹จ๊ฐ€๊ณ„์•ฝ", "์žฅ๊ธฐ๊ณ„์•ฝ", "์ŠคํŒŸ"]),
"discount_rate": round(random.random() * 0.1, 2) if rank == 0 else 0,
"quality_grade": random.choice(["A", "A", "B", "C"]),
})
supplier_offers = pd.DataFrame(offers)
# Policies
policies = pd.DataFrame([
{
"policy_id": "R-001",
"rule_name": "๊ทœ์ œํ’ˆ๋ชฉ ํ•ด์™ธ๊ตฌ๋งค ์ œํ•œ",
"rule_logic": "IF item.risk_class == '๊ทœ์ œ' AND supplier.region == 'ํ•ด์™ธ' THEN block",
"severity": "์ฐจ๋‹จ",
"department": "๋ฒ•๋ฌดํŒ€"
},
{
"policy_id": "R-002",
"rule_name": "์•ˆ์ „์žฌ๊ณ  ๋ฏธ๋งŒ ๊ธด๊ธ‰๋ฐœ์ฃผ",
"rule_logic": "IF (on_hand - reserved) < safety_stock THEN expedite",
"severity": "๊ฒฝ๊ณ ",
"department": "MROํŒ€"
},
{
"policy_id": "R-003",
"rule_name": "๊ธด๊ธ‰์„ค๋น„ ์šฐ์„ ๋ฐฐ๋ถ„",
"rule_logic": "IF equipment.criticality == '๊ธด๊ธ‰' THEN priority",
"severity": "์šฐ์„ ์ˆœ์œ„",
"department": "์ƒ์‚ฐํŒ€"
},
{
"policy_id": "R-004",
"rule_name": "ESG C๋“ฑ๊ธ‰ ์ œํ•œ",
"rule_logic": "IF supplier.esg_level == 'C' THEN penalize",
"severity": "ํŒจ๋„ํ‹ฐ",
"department": "๊ตฌ๋งคํŒ€"
},
])
# Purchase history
purchase_history = []
for i in range(200):
item = items.sample(1).iloc[0]
supplier = suppliers.sample(1).iloc[0]
qty = random.randint(10, 100)
price = random.randint(10000, 100000)
purchase_history.append({
"po_id": f"PO-2024-{10000 + i}",
"date": (datetime.now() - timedelta(days=random.randint(1, 365))).strftime("%Y-%m-%d"),
"item_id": item["item_id"],
"supplier_id": supplier["supplier_id"],
"qty": qty,
"unit_price": price,
"total_amount": qty * price,
"delivery_status": random.choice(["์™„๋ฃŒ", "์™„๋ฃŒ", "์™„๋ฃŒ", "์ง€์—ฐ", "์ง„ํ–‰์ค‘"]),
})
purchase_history = pd.DataFrame(purchase_history)
return {
"plants": plants,
"equipment": equipment,
"items": items,
"compat": compat,
"storages": storages,
"inventory": inventory,
"suppliers": suppliers,
"supplier_offers": supplier_offers,
"policies": policies,
"purchase_history": purchase_history
}
def validate_tables(tables: Dict[str, pd.DataFrame]) -> Tuple[bool, List[str]]:
"""Validate tables"""
required = ["plants", "equipment", "items", "compat", "storages", "inventory",
"suppliers", "supplier_offers", "policies", "purchase_history"]
issues = []
for k in required:
if k not in tables:
issues.append(f"Missing: {k}")
elif not isinstance(tables[k], pd.DataFrame):
issues.append(f"Invalid type: {k}")
elif len(tables[k]) == 0:
issues.append(f"Empty: {k}")
return len(issues) == 0, issues
# =========================================================
# Plotly Dashboard Functions
# =========================================================
def create_mro_inventory_dashboard(inv_df: pd.DataFrame, item_name: str) -> go.Figure:
"""MRO ์žฌ๊ณ  ํ˜„ํ™ฉ ๋Œ€์‹œ๋ณด๋“œ"""
if len(inv_df) == 0:
fig = go.Figure()
fig.add_annotation(text="์žฌ๊ณ  ๋ฐ์ดํ„ฐ ์—†์Œ", showarrow=False, font_size=20)
fig.update_layout(height=700, title_text="์žฌ๊ณ  ์ •๋ณด ์—†์Œ")
return fig
# ์„œ๋ธŒํ”Œ๋กฏ ์ƒ์„ฑ
fig = make_subplots(
rows=2, cols=2,
subplot_titles=('์ฐฝ๊ณ ๋ณ„ ์žฌ๊ณ  ํ˜„ํ™ฉ', '์•ˆ์ „์žฌ๊ณ  ๋Œ€๋น„ ํ˜„์žฌ๊ณ ', '์žฌ๊ณ  ์ƒํƒœ', '์ฐฝ๊ณ ๋ณ„ ์ ์œ ์œจ'),
specs=[[{"type": "bar"}, {"type": "indicator"}],
[{"type": "pie"}, {"type": "table"}]]
)
# 1. ์ฐฝ๊ณ ๋ณ„ ์žฌ๊ณ  ๋ฐ” ์ฐจํŠธ
fig.add_trace(
go.Bar(
x=inv_df['storage_name'],
y=inv_df['on_hand'],
name='ํ˜„์žฌ๊ณ ',
marker_color='lightblue',
text=inv_df['on_hand'],
textposition='auto',
),
row=1, col=1
)
fig.add_trace(
go.Bar(
x=inv_df['storage_name'],
y=inv_df['safety_stock'],
name='์•ˆ์ „์žฌ๊ณ ',
marker_color='orange',
text=inv_df['safety_stock'],
textposition='auto',
),
row=1, col=1
)
# 2. ์ด ์žฌ๊ณ  ๊ฒŒ์ด์ง€
total_stock = inv_df['on_hand'].sum()
total_safety = inv_df['safety_stock'].sum()
fig.add_trace(
go.Indicator(
mode="gauge+number+delta",
value=total_stock,
delta={'reference': total_safety, 'increasing': {'color': "green"}},
title={'text': f"์ด ์žฌ๊ณ ๋Ÿ‰<br><sub>{item_name}</sub>"},
gauge={
'axis': {'range': [0, total_safety * 2]},
'bar': {'color': "darkblue"},
'steps': [
{'range': [0, total_safety], 'color': "lightgray"},
{'range': [total_safety, total_safety * 1.5], 'color': "lightgreen"}
],
'threshold': {
'line': {'color': "red", 'width': 4},
'thickness': 0.75,
'value': total_safety
}
}
),
row=1, col=2
)
# 3. ์žฌ๊ณ  ์ƒํƒœ ํŒŒ์ด ์ฐจํŠธ
inv_df['available'] = inv_df['on_hand'] - inv_df['reserved']
fig.add_trace(
go.Pie(
labels=['๊ฐ€์šฉ์žฌ๊ณ ', '์˜ˆ์•ฝ๋จ', '์•ˆ์ „์žฌ๊ณ '],
values=[
inv_df['available'].sum(),
inv_df['reserved'].sum(),
max(0, total_safety - inv_df['available'].sum())
],
marker_colors=['green', 'orange', 'red'],
hole=0.3,
),
row=2, col=1
)
# 4. ์ƒ์„ธ ํ…Œ์ด๋ธ”
fig.add_trace(
go.Table(
header=dict(
values=['์ฐฝ๊ณ ', 'ํ˜„์žฌ๊ณ ', '์•ˆ์ „์žฌ๊ณ ', '์˜ˆ์•ฝ', '๊ฐ€์šฉ'],
fill_color='paleturquoise',
align='left'
),
cells=dict(
values=[
inv_df['storage_name'],
inv_df['on_hand'],
inv_df['safety_stock'],
inv_df['reserved'],
inv_df['available']
],
fill_color='lavender',
align='left'
)
),
row=2, col=2
)
fig.update_layout(
height=700,
showlegend=True,
title_text=f"๐Ÿ“ฆ MRO ์žฌ๊ณ  ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ - {item_name}",
title_font_size=20
)
return fig
def create_mro_workflow_status(equipment_info: Dict, compat_items: pd.DataFrame) -> go.Figure:
"""MRO ์›Œํฌํ”Œ๋กœ์šฐ ์ƒํƒœ ์‹œ๊ฐํ™”"""
fig = go.Figure()
# ์›Œํฌํ”Œ๋กœ์šฐ ๋‹จ๊ณ„
steps = [
"์„ค๋น„ ํ™•์ธ",
"ํ˜ธํ™˜๋ถ€ํ’ˆ ์กฐํšŒ",
"์žฌ๊ณ  ํ™•์ธ",
"์ˆ˜์š” ๊ฒ€์ฆ",
"๋ฐœ์ฃผ ์š”์ฒญ"
]
statuses = ["์™„๋ฃŒ", "์™„๋ฃŒ", "์ง„ํ–‰์ค‘", "๋Œ€๊ธฐ", "๋Œ€๊ธฐ"]
colors = ["green", "green", "orange", "gray", "gray"]
# Funnel ์ฐจํŠธ๋กœ ์›Œํฌํ”Œ๋กœ์šฐ ํ‘œํ˜„
fig.add_trace(go.Funnel(
y=steps,
x=[100, 80, 60, 40, 20],
textposition="inside",
textinfo="label+percent initial",
marker={"color": colors},
connector={"line": {"color": "royalblue", "width": 3}}
))
equipment_name = equipment_info.get('equipment_name', 'N/A') if equipment_info else 'N/A'
fig.update_layout(
title_text=f"๐Ÿ”„ MRO ์›Œํฌํ”Œ๋กœ์šฐ ์ง„ํ–‰ ์ƒํƒœ<br><sub>์„ค๋น„: {equipment_name}</sub>",
height=400,
showlegend=False
)
return fig
def create_procurement_comparison_dashboard(offers_df: pd.DataFrame, rules_eval: Dict) -> go.Figure:
"""๊ตฌ๋งค ๋‹ด๋‹น์ž - ๊ณต๊ธ‰์—…์ฒด ๋น„๊ต ๋Œ€์‹œ๋ณด๋“œ"""
if len(offers_df) == 0:
fig = go.Figure()
fig.add_annotation(text="๊ณต๊ธ‰์—…์ฒด ๋ฐ์ดํ„ฐ ์—†์Œ", showarrow=False, font_size=20)
fig.update_layout(height=700, title_text="๊ณต๊ธ‰์—…์ฒด ์ •๋ณด ์—†์Œ")
return fig
# ์„œ๋ธŒํ”Œ๋กฏ
fig = make_subplots(
rows=2, cols=2,
subplot_titles=(
'๐Ÿ’ฐ ๊ฐ€๊ฒฉ ๋น„๊ต',
'โฑ๏ธ ๋‚ฉ๊ธฐ ๋น„๊ต',
'๐Ÿ“Š ESG ๋“ฑ๊ธ‰ ๋ถ„ํฌ',
'๐ŸŽฏ ์ข…ํ•ฉ ํ‰๊ฐ€'
),
specs=[
[{"type": "bar"}, {"type": "scatter"}],
[{"type": "pie"}, {"type": "table"}]
]
)
# ๊ทœ์น™ ํ‰๊ฐ€ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
offers_df['blocked'] = offers_df['supplier_id'].apply(
lambda x: rules_eval.get(x, {}).get('block', False)
)
offers_df['color'] = offers_df['blocked'].apply(lambda x: 'red' if x else 'green')
# 1. ๊ฐ€๊ฒฉ ๋น„๊ต ๋ฐ” ์ฐจํŠธ
fig.add_trace(
go.Bar(
x=offers_df['supplier_name'],
y=offers_df['unit_price'],
marker_color=offers_df['color'],
text=[f"{p:,}์›" for p in offers_df['unit_price']],
textposition='auto',
name='๋‹จ๊ฐ€',
),
row=1, col=1
)
# 2. ๊ฐ€๊ฒฉ-๋‚ฉ๊ธฐ ์Šค์บํ„ฐ
fig.add_trace(
go.Scatter(
x=offers_df['lead_time_days'],
y=offers_df['unit_price'],
mode='markers+text',
marker=dict(
size=15,
color=offers_df['color'],
line=dict(width=2, color='white')
),
text=offers_df['supplier_name'],
textposition="top center",
name='๊ณต๊ธ‰์—…์ฒด',
),
row=1, col=2
)
# 3. ESG ๋“ฑ๊ธ‰ ํŒŒ์ด
esg_counts = offers_df['esg_level'].value_counts()
fig.add_trace(
go.Pie(
labels=esg_counts.index,
values=esg_counts.values,
marker_colors=['lightgreen', 'lightyellow', 'lightcoral'],
hole=0.3,
),
row=2, col=1
)
# 4. ์ข…ํ•ฉ ํ‰๊ฐ€ ํ…Œ์ด๋ธ”
evaluation = offers_df.copy()
evaluation['์ข…ํ•ฉ์ ์ˆ˜'] = (
(100 - (evaluation['unit_price'] / evaluation['unit_price'].max() * 50)) +
(100 - (evaluation['lead_time_days'] / evaluation['lead_time_days'].max() * 30)) +
evaluation['esg_level'].map({'A': 20, 'B': 10, 'C': 0})
).round(1)
evaluation['์ˆœ์œ„'] = evaluation['์ข…ํ•ฉ์ ์ˆ˜'].rank(ascending=False).astype(int)
fig.add_trace(
go.Table(
header=dict(
values=['์ˆœ์œ„', '๊ณต๊ธ‰์—…์ฒด', '๋‹จ๊ฐ€', '๋‚ฉ๊ธฐ', 'ESG', '์ ์ˆ˜'],
fill_color='paleturquoise',
align='center'
),
cells=dict(
values=[
evaluation['์ˆœ์œ„'],
evaluation['supplier_name'],
[f"{p:,}" for p in evaluation['unit_price']],
[f"{d}์ผ" for d in evaluation['lead_time_days']],
evaluation['esg_level'],
evaluation['์ข…ํ•ฉ์ ์ˆ˜']
],
fill_color=[['white' if not b else 'lightcoral' for b in evaluation['blocked']]],
align='center'
)
),
row=2, col=2
)
fig.update_layout(
height=700,
showlegend=False,
title_text="๐Ÿ“Š ๊ณต๊ธ‰์—…์ฒด ์ข…ํ•ฉ ๋น„๊ต ๋Œ€์‹œ๋ณด๋“œ",
title_font_size=20
)
fig.update_xaxes(title_text="๋‚ฉ๊ธฐ (์ผ)", row=1, col=2)
fig.update_yaxes(title_text="๋‹จ๊ฐ€ (์›)", row=1, col=2)
return fig
def create_procurement_workflow(opt_result: Dict) -> go.Figure:
"""๊ตฌ๋งค ์›Œํฌํ”Œ๋กœ์šฐ ์ง„ํ–‰ ์ƒํƒœ"""
fig = go.Figure()
# ์›Œํฌํ”Œ๋กœ์šฐ ๋‹จ๊ณ„์™€ ์ƒํƒœ
workflow_steps = [
{"step": "1. ์ˆ˜์š” ์ ‘์ˆ˜", "status": "์™„๋ฃŒ", "time": "10๋ถ„"},
{"step": "2. ๊ณต๊ธ‰์—…์ฒด ์กฐํšŒ", "status": "์™„๋ฃŒ", "time": "5๋ถ„"},
{"step": "3. ๊ทœ์ • ๊ฒ€์ฆ", "status": "์™„๋ฃŒ", "time": "2๋ถ„"},
{"step": "4. ์ตœ์ ํ™” ๋ถ„์„", "status": "์™„๋ฃŒ", "time": "3๋ถ„"},
{"step": "5. ๋ฐœ์ฃผ ์Šน์ธ", "status": "๋Œ€๊ธฐ์ค‘", "time": "-"},
{"step": "6. PO ๋ฐœํ–‰", "status": "๋Œ€๊ธฐ์ค‘", "time": "-"},
]
# Progress Bar ์Šคํƒ€์ผ
y_pos = list(range(len(workflow_steps)))
colors = []
for step_info in workflow_steps:
if step_info["status"] == "์™„๋ฃŒ":
colors.append("lightgreen")
elif step_info["status"] == "์ง„ํ–‰์ค‘":
colors.append("lightyellow")
else:
colors.append("lightgray")
fig.add_trace(go.Bar(
y=[s["step"] for s in workflow_steps],
x=[100 if s["status"] == "์™„๋ฃŒ" else 50 if s["status"] == "์ง„ํ–‰์ค‘" else 0
for s in workflow_steps],
orientation='h',
marker=dict(color=colors),
text=[f"{s['status']} ({s['time']})" for s in workflow_steps],
textposition='auto',
))
fig.update_layout(
title_text="๐Ÿ”„ ๊ตฌ๋งค ์›Œํฌํ”Œ๋กœ์šฐ ์ง„ํ–‰ ํ˜„ํ™ฉ",
xaxis_title="์ง„ํ–‰๋ฅ  (%)",
height=400,
showlegend=False
)
return fig
def create_executive_kpi_dashboard(
opt_result: Dict,
offers_df: pd.DataFrame,
purchase_history: pd.DataFrame
) -> go.Figure:
"""๊ฒฝ์˜์ง„ KPI ๋Œ€์‹œ๋ณด๋“œ"""
fig = make_subplots(
rows=2, cols=3,
subplot_titles=(
'๐Ÿ’ฐ ๋น„์šฉ ์ ˆ๊ฐ',
'โš–๏ธ ์ปดํ”Œ๋ผ์ด์–ธ์Šค',
'๐Ÿ“ˆ ESG ์ ์ˆ˜',
'โฑ๏ธ ์ฒ˜๋ฆฌ ์‹œ๊ฐ„',
'๐ŸŽฏ ๋ชฉํ‘œ ๋‹ฌ์„ฑ๋ฅ ',
'๐Ÿ“Š ์›”๊ฐ„ ํŠธ๋ Œ๋“œ'
),
specs=[
[{"type": "indicator"}, {"type": "indicator"}, {"type": "indicator"}],
[{"type": "indicator"}, {"type": "indicator"}, {"type": "scatter"}]
]
)
# 1. ๋น„์šฉ ์ ˆ๊ฐ
if len(offers_df) > 0:
min_price = offers_df['unit_price'].min()
max_price = offers_df['unit_price'].max()
savings = ((max_price - min_price) / max_price * 100) if max_price > 0 else 0
else:
savings = 0
fig.add_trace(
go.Indicator(
mode="gauge+number+delta",
value=savings,
title={'text': "๋น„์šฉ ์ ˆ๊ฐ๋ฅ  (%)"},
delta={'reference': 10},
gauge={
'axis': {'range': [0, 50]},
'bar': {'color': "darkblue"},
'steps': [
{'range': [0, 10], 'color': "lightgray"},
{'range': [10, 25], 'color': "lightgreen"},
{'range': [25, 50], 'color': "green"}
],
'threshold': {
'line': {'color': "red", 'width': 4},
'thickness': 0.75,
'value': 15
}
}
),
row=1, col=1
)
# 2. ์ปดํ”Œ๋ผ์ด์–ธ์Šค ์ค€์ˆ˜์œจ
fig.add_trace(
go.Indicator(
mode="gauge+number",
value=100,
title={'text': "๊ทœ์ • ์ค€์ˆ˜์œจ (%)"},
gauge={
'axis': {'range': [0, 100]},
'bar': {'color': "green"},
'steps': [
{'range': [0, 80], 'color': "lightcoral"},
{'range': [80, 95], 'color': "lightyellow"},
{'range': [95, 100], 'color': "lightgreen"}
]
}
),
row=1, col=2
)
# 3. ESG ํ‰๊ท  ์ ์ˆ˜
if len(offers_df) > 0:
esg_score = offers_df['esg_level'].map({'A': 100, 'B': 70, 'C': 40}).mean()
else:
esg_score = 0
fig.add_trace(
go.Indicator(
mode="gauge+number",
value=esg_score,
title={'text': "ESG ํ‰๊ท  ์ ์ˆ˜"},
gauge={
'axis': {'range': [0, 100]},
'bar': {'color': "darkgreen"},
'steps': [
{'range': [0, 50], 'color': "lightcoral"},
{'range': [50, 80], 'color': "lightyellow"},
{'range': [80, 100], 'color': "lightgreen"}
]
}
),
row=1, col=3
)
# 4. ํ‰๊ท  ์ฒ˜๋ฆฌ ์‹œ๊ฐ„
fig.add_trace(
go.Indicator(
mode="number+delta",
value=20,
title={'text': "์ฒ˜๋ฆฌ ์‹œ๊ฐ„ (๋ถ„)"},
delta={'reference': 30, 'increasing': {'color': "red"}, 'decreasing': {'color': "green"}},
number={'suffix': "๋ถ„"}
),
row=2, col=1
)
# 5. ๋ชฉํ‘œ ๋‹ฌ์„ฑ๋ฅ 
fig.add_trace(
go.Indicator(
mode="gauge+number",
value=85,
title={'text': "์›”๊ฐ„ ๋ชฉํ‘œ ๋‹ฌ์„ฑ๋ฅ  (%)"},
gauge={
'axis': {'range': [0, 100]},
'bar': {'color': "royalblue"},
'threshold': {
'line': {'color': "red", 'width': 4},
'thickness': 0.75,
'value': 80
}
}
),
row=2, col=2
)
# 6. ์›”๊ฐ„ ํŠธ๋ Œ๋“œ
months = ['1์›”', '2์›”', '3์›”', '4์›”', '5์›”', '6์›”']
values = [75, 78, 82, 85, 88, 90]
fig.add_trace(
go.Scatter(
x=months,
y=values,
mode='lines+markers',
name='๋ฐœ์ฃผ ํšจ์œจ',
line=dict(color='royalblue', width=3),
marker=dict(size=10)
),
row=2, col=3
)
fig.update_layout(
height=700,
showlegend=False,
title_text="๐Ÿ“Š ๊ฒฝ์˜์ง„ KPI ๋Œ€์‹œ๋ณด๋“œ",
title_font_size=22
)
return fig
def create_action_items_table(opt_result: Dict, offers_df: pd.DataFrame) -> pd.DataFrame:
"""๊ฒฝ์˜์ง„ Action Items ์ƒ์„ฑ"""
action_items = []
# 1. ์ฆ‰์‹œ ๋ฐœ์ฃผ ์Šน์ธ ํ•ญ๋ชฉ
alloc = opt_result.get('allocation', {})
if alloc:
for supplier_id, details in alloc.items():
if isinstance(details, dict):
action_items.append({
"์šฐ์„ ์ˆœ์œ„": "๐Ÿ”ด ๊ธด๊ธ‰",
"Action Item": f"{details.get('supplier_name')} ๋ฐœ์ฃผ ์Šน์ธ",
"์ˆ˜๋Ÿ‰": f"{details.get('qty')}๊ฐœ",
"์˜ˆ์ƒ ๋น„์šฉ": f"{details.get('qty', 0) * details.get('unit_price', 0):,}์›",
"๋‹ด๋‹น": "๊ตฌ๋งคํŒ€",
"๊ธฐํ•œ": "์ฆ‰์‹œ",
"์ƒํƒœ": "์Šน์ธ ๋Œ€๊ธฐ"
})
# 2. ์žฌ๊ณ  ๋ณด์ถฉ ๊ถŒ๊ณ 
action_items.append({
"์šฐ์„ ์ˆœ์œ„": "๐ŸŸก ์ค‘์š”",
"Action Item": "์•ˆ์ „์žฌ๊ณ  ๋ฏธ๋‹ฌ ํ’ˆ๋ชฉ ๋ณด์ถฉ",
"์ˆ˜๋Ÿ‰": "3๊ฐœ ํ’ˆ๋ชฉ",
"์˜ˆ์ƒ ๋น„์šฉ": "๊ฒ€ํ†  ํ•„์š”",
"๋‹ด๋‹น": "MROํŒ€",
"๊ธฐํ•œ": "1์ฃผ์ผ ๋‚ด",
"์ƒํƒœ": "๊ฒ€ํ†  ์ค‘"
})
# 3. ESG ๊ฐœ์„ 
if len(offers_df) > 0:
c_grade_count = len(offers_df[offers_df['esg_level'] == 'C'])
if c_grade_count > 0:
action_items.append({
"์šฐ์„ ์ˆœ์œ„": "๐ŸŸข ๋ณดํ†ต",
"Action Item": "ESG C๋“ฑ๊ธ‰ ๊ณต๊ธ‰์—…์ฒด ๋Œ€์ฒด ๊ฒ€ํ† ",
"์ˆ˜๋Ÿ‰": f"{c_grade_count}๊ฐœ์‚ฌ",
"์˜ˆ์ƒ ๋น„์šฉ": "์˜ํ–ฅ๋„ ๋ถ„์„ ํ•„์š”",
"๋‹ด๋‹น": "๊ตฌ๋งคํŒ€",
"๊ธฐํ•œ": "1๊ฐœ์›” ๋‚ด",
"์ƒํƒœ": "๊ณ„ํš ๋‹จ๊ณ„"
})
# 4. ์žฅ๊ธฐ ๊ณ„์•ฝ ํ˜‘์ƒ
action_items.append({
"์šฐ์„ ์ˆœ์œ„": "๐ŸŸข ๋ณดํ†ต",
"Action Item": "์ฃผ์š” ๊ณต๊ธ‰์—…์ฒด ์žฅ๊ธฐ๊ณ„์•ฝ ํ˜‘์ƒ",
"์ˆ˜๋Ÿ‰": "2-3๊ฐœ์‚ฌ",
"์˜ˆ์ƒ ๋น„์šฉ": "5-10% ์ ˆ๊ฐ ์˜ˆ์ƒ",
"๋‹ด๋‹น": "๊ตฌ๋งคํŒ€",
"๊ธฐํ•œ": "๋ถ„๊ธฐ ๋‚ด",
"์ƒํƒœ": "๊ณ„ํš ๋‹จ๊ณ„"
})
return pd.DataFrame(action_items)
# =========================================================
# Core Components
# =========================================================
@dataclass
class ToolCallLog:
ts: str
actor: str
tool: str
input: Dict[str, Any]
output_preview: str
class MCPToolRegistry:
def __init__(self, tables: Dict[str, pd.DataFrame]):
self.tables = tables
self.logs: List[ToolCallLog] = []
def _log(self, actor: str, tool: str, inp: Dict[str, Any], out: Any):
self.logs.append(ToolCallLog(
ts=now_ts(),
actor=actor,
tool=tool,
input=inp,
output_preview=str(out)[:500]
))
def query_inventory(self, actor: str, item_id: str) -> pd.DataFrame:
inv = self.tables["inventory"]
stor = self.tables["storages"]
df = inv[inv["item_id"] == item_id].copy()
if len(df) > 0:
df = df.merge(stor, on="storage_id", how="left")
self._log(actor, "query_inventory", {"item_id": item_id}, f"{len(df)} rows")
return df
def query_offers(self, actor: str, item_id: str) -> pd.DataFrame:
offers = self.tables["supplier_offers"]
suppliers = self.tables["suppliers"]
df = offers[offers["item_id"] == item_id].copy()
if len(df) > 0:
df = df.merge(suppliers, on="supplier_id", how="left")
self._log(actor, "query_offers", {"item_id": item_id}, f"{len(df)} rows")
return df
def query_compat_items(self, actor: str, equipment_id: str) -> pd.DataFrame:
compat = self.tables["compat"]
items = self.tables["items"]
df = compat[compat["equipment_id"] == equipment_id].copy()
if len(df) > 0:
df = df.merge(items, on="item_id", how="left")
self._log(actor, "query_compat_items", {"equipment_id": equipment_id}, f"{len(df)} rows")
return df
def get_equipment_info(self, actor: str, equipment_id: str) -> Dict[str, Any]:
eq = self.tables["equipment"]
match = eq[eq["equipment_id"] == equipment_id]
if len(match) == 0:
return {}
info = match.iloc[0].to_dict()
self._log(actor, "get_equipment_info", {"equipment_id": equipment_id}, safe_json(info))
return info
def audit_log_df(self) -> pd.DataFrame:
if not self.logs:
return pd.DataFrame({"๋ฉ”์‹œ์ง€": ["๋กœ๊ทธ ์—†์Œ"]})
return pd.DataFrame([{
"์‹œ๊ฐ„": l.ts[:19],
"์—์ด์ „ํŠธ": l.actor,
"๋„๊ตฌ": l.tool,
"์ž…๋ ฅ": str(l.input)[:50],
} for l in self.logs])
def apply_rules(tables: Dict[str, pd.DataFrame], item_id: str,
supplier_row: Dict[str, Any]) -> Dict[str, Any]:
"""Apply rules"""
items = tables["items"]
item_match = items[items["item_id"] == item_id]
if len(item_match) == 0:
return {"block": False, "alerts": [], "explanations": [], "rules_fired": []}
item = item_match.iloc[0].to_dict()
decision = {
"block": False,
"alerts": [],
"explanations": [],
"rules_fired": []
}
if item.get("risk_class") == "๊ทœ์ œ" and supplier_row.get("region") == "ํ•ด์™ธ":
decision["block"] = True
decision["rules_fired"].append("R-001")
decision["explanations"].append(
f"๐Ÿšซ R-001: ๊ทœ์ œํ’ˆ๋ชฉ({item.get('item_name')}) ํ•ด์™ธ์—…์ฒด({supplier_row.get('supplier_name')}) ๊ตฌ๋งค ์ฐจ๋‹จ"
)
if supplier_row.get("esg_level") == "C":
decision["rules_fired"].append("R-004")
decision["explanations"].append(
f"๐Ÿ“Š R-004: ESG C๋“ฑ๊ธ‰({supplier_row.get('supplier_name')}) ํŒจ๋„ํ‹ฐ"
)
return decision
def optimize_order_allocation(demand_qty: int, offers_df: pd.DataFrame,
rules_eval: Dict[str, Dict[str, Any]]) -> Dict[str, Any]:
"""Optimize allocation"""
if not PULP_AVAILABLE:
return {
"status": "UNAVAILABLE",
"reason": "PuLP not installed",
"allocation": {},
"demand": demand_qty
}
feasible = []
blocked = []
for _, r in offers_df.iterrows():
sid = r["supplier_id"]
if rules_eval.get(sid, {}).get("block"):
blocked.append({
"supplier_id": sid,
"supplier_name": r.get("supplier_name", sid),
"reason": "๊ทœ์น™ ์œ„๋ฐ˜"
})
else:
feasible.append(r)
if len(feasible) == 0:
return {
"status": "INFEASIBLE",
"reason": "๋ชจ๋“  ๊ณต๊ธ‰์—…์ฒด ์ฐจ๋‹จ",
"allocation": {},
"blocked_suppliers": blocked,
"demand": demand_qty
}
fdf = pd.DataFrame(feasible)
prob = LpProblem("MRO_Allocation", LpMinimize)
x = {}
for _, r in fdf.iterrows():
sid = r["supplier_id"]
x[sid] = LpVariable(f"x_{sid}", lowBound=0, cat="Integer")
prob += lpSum(list(x.values())) >= demand_qty, "DemandConstraint"
obj_terms = []
for _, r in fdf.iterrows():
sid = r["supplier_id"]
price = float(r["unit_price"])
obj_terms.append(x[sid] * price)
prob += lpSum(obj_terms), "TotalCost"
prob.solve()
alloc = {}
total_cost = 0.0
for _, r in fdf.iterrows():
sid = r["supplier_id"]
val = x[sid].value()
if val is not None and val > 0:
qty = int(val)
alloc[sid] = {
"qty": qty,
"unit_price": float(r["unit_price"]),
"supplier_name": r.get("supplier_name", sid),
"lead_time": int(r.get("lead_time_days", 0))
}
total_cost += qty * float(r["unit_price"])
return {
"status": LpStatus.get(prob.status, "Unknown"),
"allocation": alloc,
"demand": demand_qty,
"blocked_suppliers": blocked,
"total_cost": round(total_cost, 2)
}
class LLMOrchestrator:
def __init__(self):
self.api_key = os.environ.get("OPENAI_API_KEY", "").strip()
self.demo_mode = (not self.api_key or not OPENAI_AVAILABLE)
if not self.demo_mode:
try:
self.client = OpenAI(api_key=self.api_key)
test_resp = self.client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "test"}],
max_tokens=5
)
print("โœ… OpenAI API ์—ฐ๊ฒฐ ์„ฑ๊ณต!")
except Exception:
self.demo_mode = True
self.client = None
else:
self.client = None
def chat(self, role: str, system: str, user: str) -> str:
if self.demo_mode:
return self._demo_response(role)
try:
resp = self.client.chat.completions.create(
model="gpt-4o-mini",
temperature=0.2,
messages=[
{"role": "system", "content": system},
{"role": "user", "content": user}
]
)
return resp.choices[0].message.content
except Exception as e:
return f"[ERROR: {e}]\n" + self._demo_response(role)
def _demo_response(self, role: str) -> str:
return f"[DEMO MODE - {role}] AI ๋ถ„์„ ์™„๋ฃŒ"
# =========================================================
# LangGraph Workflow
# =========================================================
class DemoState(TypedDict, total=False):
tables: Dict[str, pd.DataFrame]
mcp: MCPToolRegistry
llm: LLMOrchestrator
scenario: str
equipment_id: str
item_id: str
demand_qty: int
priority: str
tables_ok: bool
validation_issues: List[str]
progress: str
inventory_view: pd.DataFrame
offers_view: pd.DataFrame
rules_eval: Dict[str, Any]
optimization: Dict[str, Any]
narrative: Dict[str, str]
audit_log: pd.DataFrame
selected_item_name: str
equipment_info: Dict[str, Any]
compat_items: pd.DataFrame
def node_validate(state: DemoState) -> DemoState:
ok, issues = validate_tables(state["tables"])
state["tables_ok"] = ok
state["validation_issues"] = issues
state["progress"] = "1/4 ๊ฒ€์ฆ ์™„๋ฃŒ"
return state
def node_mro_agent(state: DemoState) -> DemoState:
mcp: MCPToolRegistry = state["mcp"]
equipment_id = state.get("equipment_id", "")
item_id = state.get("item_id", "")
# Get equipment info
equipment_info = mcp.get_equipment_info("MRO_AGENT", equipment_id)
state["equipment_info"] = equipment_info
# Get compatible items
compat_df = pd.DataFrame()
if equipment_id:
compat_df = mcp.query_compat_items("MRO_AGENT", equipment_id)
state["compat_items"] = compat_df
if not item_id and len(compat_df) > 0:
mandatory = compat_df[compat_df["is_mandatory"] == True]
if len(mandatory) > 0:
selected = mandatory.iloc[0]
else:
selected = compat_df.iloc[0]
item_id = selected["item_id"]
state["item_id"] = item_id
state["selected_item_name"] = selected.get("item_name", item_id)
inv_df = pd.DataFrame()
if item_id:
inv_df = mcp.query_inventory("MRO_AGENT", item_id)
state["inventory_view"] = inv_df
llm: LLMOrchestrator = state["llm"]
if "narrative" not in state:
state["narrative"] = {}
state["narrative"]["mro"] = llm.chat("MRO", "MRO ๋ถ„์„", "์„ค๋น„/์žฌ๊ณ ")
state["progress"] = "2/4 MRO ์™„๋ฃŒ"
return state
def node_procurement_agent(state: DemoState) -> DemoState:
mcp: MCPToolRegistry = state["mcp"]
item_id = state.get("item_id", "")
demand_qty = int(state.get("demand_qty", 10))
offers_df = pd.DataFrame()
if item_id:
offers_df = mcp.query_offers("PROC_AGENT", item_id)
state["offers_view"] = offers_df
rules_eval = {}
if len(offers_df) > 0:
for _, r in offers_df.iterrows():
sid = r["supplier_id"]
supplier_row = {
"supplier_id": sid,
"supplier_name": r.get("supplier_name", sid),
"region": r.get("region", ""),
"esg_level": r.get("esg_level", ""),
}
rules_eval[sid] = apply_rules(state["tables"], item_id, supplier_row)
state["rules_eval"] = rules_eval
opt_result = {}
if len(offers_df) > 0:
opt_result = optimize_order_allocation(demand_qty, offers_df, rules_eval)
else:
opt_result = {
"status": "NO_DATA",
"reason": "๊ณต๊ธ‰์—…์ฒด ์ •๋ณด ์—†์Œ",
"allocation": {},
"demand": demand_qty
}
state["optimization"] = opt_result
llm: LLMOrchestrator = state["llm"]
state["narrative"]["proc"] = llm.chat("PROC", "๊ตฌ๋งค ์ „๋žต", "์ตœ์ ํ™”")
state["narrative"]["exec"] = llm.chat("EXEC", "์ž„์› ์š”์•ฝ", "์ข…ํ•ฉ")
state["progress"] = "3/4 ๊ตฌ๋งค ์™„๋ฃŒ"
return state
def node_collect_audit(state: DemoState) -> DemoState:
mcp: MCPToolRegistry = state["mcp"]
state["audit_log"] = mcp.audit_log_df()
state["progress"] = "4/4 ์™„๋ฃŒ โœ“"
return state
def build_workflow():
if not LANGGRAPH_AVAILABLE:
return None
try:
graph = StateGraph(DemoState)
graph.add_node("validate", node_validate)
graph.add_node("mro_agent", node_mro_agent)
graph.add_node("procurement_agent", node_procurement_agent)
graph.add_node("collect_audit", node_collect_audit)
graph.set_entry_point("validate")
graph.add_edge("validate", "mro_agent")
graph.add_edge("mro_agent", "procurement_agent")
graph.add_edge("procurement_agent", "collect_audit")
graph.add_edge("collect_audit", END)
return graph.compile()
except Exception as e:
print(f"โš ๏ธ LangGraph failed: {e}")
return None
APP = build_workflow()
# =========================================================
# Main Execution - Enhanced with Dashboards
# =========================================================
def run_demo(scenario: str, seed: int, equipment_id: str, item_id: str,
demand_qty: int) -> Tuple:
"""Main execution - returns 12 outputs (enhanced)"""
try:
seed_int = int(seed)
demand_int = int(demand_qty)
tables = generate_demo_tables(seed=seed_int)
mcp = MCPToolRegistry(tables)
llm = LLMOrchestrator()
preset = SCENARIO_PRESETS.get(scenario, SCENARIO_PRESETS["๊ธด๊ธ‰ ๊ณ ์žฅ ๋Œ€์‘"])
state: DemoState = {
"tables": tables,
"mcp": mcp,
"llm": llm,
"scenario": scenario,
"equipment_id": equipment_id.strip(),
"item_id": item_id.strip(),
"demand_qty": demand_int,
"priority": preset.get("priority", "์ •์ƒ"),
}
if APP is not None:
out = APP.invoke(state)
else:
out = node_validate(state)
out = node_mro_agent(out)
out = node_procurement_agent(out)
out = node_collect_audit(out)
status = {
"mode": "โš ๏ธ DEMO" if llm.demo_mode else "โœ… LLM",
"scenario": scenario,
"tables_ok": out.get("tables_ok", False),
"equipment": out.get("equipment_id", ""),
"item_name": out.get("selected_item_name", ""),
"demand": out.get("demand_qty", 0),
"priority": out.get("priority", "์ •์ƒ"),
"progress": out.get("progress", "์™„๋ฃŒ"),
}
status_text = format_status(status)
# Data extraction
inv_df = out.get("inventory_view", pd.DataFrame())
offers_df = out.get("offers_view", pd.DataFrame())
audit_df = out.get("audit_log", pd.DataFrame())
equipment_info = out.get("equipment_info", {})
compat_items = out.get("compat_items", pd.DataFrame())
rules_eval = out.get("rules_eval", {})
opt_result = out.get("optimization", {})
purchase_history = tables.get("purchase_history", pd.DataFrame())
item_name = out.get("selected_item_name", "๋ถ€ํ’ˆ")
# Create dashboards
mro_dashboard = create_mro_inventory_dashboard(inv_df, item_name)
mro_workflow = create_mro_workflow_status(equipment_info, compat_items)
proc_dashboard = create_procurement_comparison_dashboard(offers_df, rules_eval)
proc_workflow = create_procurement_workflow(opt_result)
exec_dashboard = create_executive_kpi_dashboard(opt_result, offers_df, purchase_history)
action_items = create_action_items_table(opt_result, offers_df)
# Fallback for empty dataframes
if len(audit_df) == 0:
audit_df = pd.DataFrame({"๋ฉ”์‹œ์ง€": ["๊ฐ์‚ฌ๋กœ๊ทธ ์—†์Œ"]})
print("โœ… ๋Œ€์‹œ๋ณด๋“œ ์ƒ์„ฑ ์™„๋ฃŒ\n")
# Return 12 outputs
return (
status_text, # 1
mro_dashboard, # 2 - MRO ์žฌ๊ณ  ๋Œ€์‹œ๋ณด๋“œ
mro_workflow, # 3 - MRO ์›Œํฌํ”Œ๋กœ์šฐ
proc_dashboard, # 4 - ๊ตฌ๋งค ๋น„๊ต ๋Œ€์‹œ๋ณด๋“œ
proc_workflow, # 5 - ๊ตฌ๋งค ์›Œํฌํ”Œ๋กœ์šฐ
exec_dashboard, # 6 - ๊ฒฝ์˜์ง„ KPI
action_items, # 7 - Action Items
offers_df, # 8 - ๊ณต๊ธ‰์—…์ฒด ์›๋ณธ ๋ฐ์ดํ„ฐ
inv_df, # 9 - ์žฌ๊ณ  ์›๋ณธ ๋ฐ์ดํ„ฐ
opt_result, # 10 - ์ตœ์ ํ™” ๊ฒฐ๊ณผ (dict๋ฅผ text๋กœ)
audit_df, # 11 - ๊ฐ์‚ฌ ๋กœ๊ทธ
out.get("selected_item_name", "N/A") # 12 - ํ’ˆ๋ชฉ๋ช…
)
except Exception as e:
print(f"โŒ ์˜ค๋ฅ˜: {e}\n{traceback.format_exc()}")
error_msg = f"โŒ ์˜ค๋ฅ˜\n\n{str(e)}"
empty_fig = go.Figure()
empty_fig.add_annotation(text="์˜ค๋ฅ˜ ๋ฐœ์ƒ", showarrow=False)
empty_df = pd.DataFrame({"์˜ค๋ฅ˜": [str(e)[:100]]})
return (
error_msg, empty_fig, empty_fig, empty_fig, empty_fig,
empty_fig, empty_df, empty_df, empty_df, {}, empty_df, "N/A"
)
def update_scenario(scenario: str) -> Tuple[str, str, int, str]:
preset = SCENARIO_PRESETS.get(scenario, SCENARIO_PRESETS["๊ธด๊ธ‰ ๊ณ ์žฅ ๋Œ€์‘"])
guide_text = f"""**๐Ÿ“Œ {preset['description']}**
**๋ฐฐ๊ฒฝ**: {preset['context']}
**์šฐ์„ ์ˆœ์œ„**: {preset.get('priority', '์ •์ƒ')}
**๊ฐ€์ด๋“œ**: {preset.get('guide', '')}
"""
return (
preset["equipment_id"],
preset["item_id"],
preset["demand_qty"],
guide_text
)
# =========================================================
# Enhanced Gradio UI with Process Guides
# =========================================================
print("๐ŸŽจ ํ”„๋กœ์„ธ์Šค ๊ฐ€์ด๋“œ ํ†ตํ•ฉ UI ๊ตฌ์„ฑ ์ค‘...\n")
with gr.Blocks(title="POSCO DX MRO Composite AI", theme=gr.themes.Soft()) as demo:
gr.Markdown("""
# ๐Ÿญ POSCO DX - MRO Composite AI ํ”„๋กœ์„ธ์Šค ๊ฐ€์ด๋“œ ํ†ตํ•ฉ ๋ฒ„์ „
## ๐ŸŽฏ ์—…๋ฌด ํ”„๋กœ์„ธ์Šค ์ž๋™ํ™” + AI ์˜์‚ฌ๊ฒฐ์ • ์ง€์› ์‹œ์Šคํ…œ
**3-Agent Collaboration**: MRO ์šด์˜ โ†’ ๊ตฌ๋งค/์กฐ๋‹ฌ โ†’ ๊ฒฝ์˜์ง„ ์Šน์ธ
""")
# Process Overview Section
with gr.Accordion("๐Ÿ“– ์ „์ฒด ์—…๋ฌด ํ”„๋กœ์„ธ์Šค ๊ฐœ์š”", open=False):
gr.Markdown("""
### ๐Ÿ”„ End-to-End ์›Œํฌํ”Œ๋กœ์šฐ
```
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ 1๏ธโƒฃ MRO ์šด์˜ โ”‚ โ”€โ”€โ”€> โ”‚ 2๏ธโƒฃ ๊ตฌ๋งค/์กฐ๋‹ฌ โ”‚ โ”€โ”€โ”€> โ”‚ 3๏ธโƒฃ ๊ฒฝ์˜์ง„ ์Šน์ธ โ”‚
โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚
โ”‚ โ€ข ๊ณ ์žฅ ์ ‘์ˆ˜ โ”‚ โ”‚ โ€ข ๊ณต๊ธ‰์—…์ฒด ์กฐํšŒ โ”‚ โ”‚ โ€ข KPI ํ™•์ธ โ”‚
โ”‚ โ€ข ๋ถ€ํ’ˆ ํ™•์ธ โ”‚ โ”‚ โ€ข ๊ทœ์ • ๊ฒ€์ฆ โ”‚ โ”‚ โ€ข ์˜์‚ฌ๊ฒฐ์ • โ”‚
โ”‚ โ€ข ์žฌ๊ณ  ํ™•์ธ โ”‚ โ”‚ โ€ข ์ตœ์ ํ™” ๋ถ„์„ โ”‚ โ”‚ โ€ข ํ”ผ๋“œ๋ฐฑ โ”‚
โ”‚ โ€ข ๋ฐœ์ฃผ ์š”์ฒญ โ”‚ โ”‚ โ€ข ์Šน์ธ ์š”์ฒญ โ”‚ โ”‚ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โฑ๏ธ 15๋ถ„ โฑ๏ธ 25๋ถ„ โฑ๏ธ 25๋ถ„
```
### ๐Ÿ’ก ํ•ต์‹ฌ ๊ฐ€์น˜
1. **์ž๋™ํ™”**: ์„ค๋น„-๋ถ€ํ’ˆ ๋งค์นญ, ์žฌ๊ณ  ์กฐํšŒ, ๊ทœ์ • ๊ฒ€์ฆ ๋“ฑ ๋ฐ˜๋ณต ์—…๋ฌด ์ž๋™ํ™”
2. **์ตœ์ ํ™”**: AI ๊ธฐ๋ฐ˜ ๋น„์šฉ ์ตœ์ ํ™” ๋ฐ ๊ณต๊ธ‰์—…์ฒด ์„ ์ •
3. **๊ฒ€์ฆ**: Neuro-Symbolic AI๋กœ 100% ๊ทœ์ • ์ค€์ˆ˜ ๋ณด์žฅ
4. **๊ฐ€์‹œ์„ฑ**: ์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ๋กœ ์ „ ๊ณผ์ • ๋ชจ๋‹ˆํ„ฐ๋ง
5. **์ถ”์ ์„ฑ**: ๋ชจ๋“  ์˜์‚ฌ๊ฒฐ์ • ๊ณผ์ • ์ž๋™ ๊ธฐ๋ก
### ๐Ÿ“Š ๊ธฐ๋Œ€ ํšจ๊ณผ
- โฑ๏ธ **์ฒ˜๋ฆฌ ์‹œ๊ฐ„**: ๊ธฐ์กด 3-5์ผ โ†’ **1์‹œ๊ฐ„ ์ด๋‚ด**
- ๐Ÿ’ฐ **๋น„์šฉ ์ ˆ๊ฐ**: ํ‰๊ท  **15-25%** ๊ตฌ๋งค ๋น„์šฉ ์ ˆ๊ฐ
- โš–๏ธ **์ปดํ”Œ๋ผ์ด์–ธ์Šค**: **100%** ๊ทœ์ • ์ค€์ˆ˜
- ๐Ÿ“ˆ **ํšจ์œจ์„ฑ**: ๋‹ด๋‹น์ž ์—…๋ฌด ์‹œ๊ฐ„ **60%** ๋‹จ์ถ•
""")
with gr.Row():
with gr.Column():
gr.Markdown("### ๐ŸŽฏ ์‹œ๋‚˜๋ฆฌ์˜ค")
scenario_radio = gr.Radio(
choices=list(SCENARIO_PRESETS.keys()),
value="๊ธด๊ธ‰ ๊ณ ์žฅ ๋Œ€์‘",
label="๋ถ„์„ ์‹œ๋‚˜๋ฆฌ์˜ค"
)
scenario_info = gr.Markdown(
value=f"""**๐Ÿ“Œ {SCENARIO_PRESETS['๊ธด๊ธ‰ ๊ณ ์žฅ ๋Œ€์‘']['description']}**
**๋ฐฐ๊ฒฝ**: {SCENARIO_PRESETS['๊ธด๊ธ‰ ๊ณ ์žฅ ๋Œ€์‘']['context']}
**๊ฐ€์ด๋“œ**: {SCENARIO_PRESETS['๊ธด๊ธ‰ ๊ณ ์žฅ ๋Œ€์‘']['guide']}
"""
)
with gr.Column():
gr.Markdown("### โš™๏ธ ํŒŒ๋ผ๋ฏธํ„ฐ")
seed_number = gr.Number(value=7, label="๋ฐ์ดํ„ฐ ์‹œ๋“œ", precision=0)
equipment_text = gr.Textbox(value="CONV-PH-007", label="์„ค๋น„ ID")
item_text = gr.Textbox(value="", label="ํ’ˆ๋ชฉ ID (์„ ํƒ)")
demand_number = gr.Number(value=10, label="์ˆ˜๋Ÿ‰", precision=0)
run_button = gr.Button("๐Ÿš€ Composite AI ๋ถ„์„ ์‹คํ–‰", variant="primary", size="lg")
gr.Markdown("---")
status_output = gr.Textbox(label="๐Ÿ“Š ์‹คํ–‰ ์ƒํƒœ", lines=10)
selected_item_display = gr.Textbox(label="๐Ÿ“ฆ ์„ ํƒ๋œ ํ’ˆ๋ชฉ", interactive=False)
with gr.Tabs():
with gr.Tab("๐Ÿ”ง MRO ๋‹ด๋‹น์ž"):
# Process Guide for MRO
with gr.Accordion("๐Ÿ“‹ MRO ์šด์˜ ํ”„๋กœ์„ธ์Šค ๊ฐ€์ด๋“œ", open=True):
mro_process_html = gr.HTML(create_process_guide_html("mro"))
gr.Markdown("---")
gr.Markdown("### ๐Ÿ“Š ๋Œ€์‹œ๋ณด๋“œ ๋ฐ ๋ถ„์„ ๊ฒฐ๊ณผ")
mro_inventory_plot = gr.Plot(label="๐Ÿ“ฆ ์žฌ๊ณ  ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ")
mro_workflow_plot = gr.Plot(label="๐Ÿ”„ MRO ์›Œํฌํ”Œ๋กœ์šฐ ์ง„ํ–‰")
mro_inventory_table = gr.Dataframe(label="๐Ÿ“‹ ์ƒ์„ธ ์žฌ๊ณ  ๋ฐ์ดํ„ฐ")
with gr.Tab("๐Ÿ’ฐ ๊ตฌ๋งค ๋‹ด๋‹น์ž"):
# Process Guide for Procurement
with gr.Accordion("๐Ÿ“‹ ๊ตฌ๋งค/์กฐ๋‹ฌ ํ”„๋กœ์„ธ์Šค ๊ฐ€์ด๋“œ", open=True):
proc_process_html = gr.HTML(create_process_guide_html("procurement"))
gr.Markdown("---")
gr.Markdown("### ๐Ÿ“Š ๋Œ€์‹œ๋ณด๋“œ ๋ฐ ๋ถ„์„ ๊ฒฐ๊ณผ")
proc_comparison_plot = gr.Plot(label="๐Ÿ“Š ๊ณต๊ธ‰์—…์ฒด ๋น„๊ต ๋Œ€์‹œ๋ณด๋“œ")
proc_workflow_plot = gr.Plot(label="๐Ÿ”„ ๊ตฌ๋งค ์›Œํฌํ”Œ๋กœ์šฐ")
proc_offers_table = gr.Dataframe(label="๐Ÿ“‹ ๊ณต๊ธ‰์—…์ฒด ์ƒ์„ธ ์ •๋ณด")
with gr.Tab("๐Ÿ‘” ๊ฒฝ์˜์ง„"):
# Process Guide for Executive
with gr.Accordion("๐Ÿ“‹ ๊ฒฝ์˜์ง„ ์˜์‚ฌ๊ฒฐ์ • ํ”„๋กœ์„ธ์Šค ๊ฐ€์ด๋“œ", open=True):
exec_process_html = gr.HTML(create_process_guide_html("executive"))
gr.Markdown("---")
gr.Markdown("### ๐Ÿ“Š ๋Œ€์‹œ๋ณด๋“œ ๋ฐ ๋ถ„์„ ๊ฒฐ๊ณผ")
exec_kpi_plot = gr.Plot(label="๐Ÿ“Š ๊ฒฝ์˜์ง„ KPI ๋Œ€์‹œ๋ณด๋“œ")
exec_action_table = gr.Dataframe(label="๐Ÿ“‹ Action Items")
gr.Markdown("### ๐Ÿ’ฌ ๊ฒฝ์˜์ง„ ํ”ผ๋“œ๋ฐฑ")
with gr.Row():
feedback_text = gr.Textbox(
label="๊ฐœ์„  ์ œ์•ˆ / ํ”ผ๋“œ๋ฐฑ",
placeholder="์˜ˆ: ESG C๋“ฑ๊ธ‰ ์—…์ฒด ๋น„์ค‘์„ 20% ์ดํ•˜๋กœ ์ œํ•œํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.",
lines=3
)
with gr.Row():
approve_btn = gr.Button("โœ… ์Šน์ธ", variant="primary")
reject_btn = gr.Button("โŒ ๋ฐ˜๋ ค", variant="stop")
suggest_btn = gr.Button("๐Ÿ’ก ๊ฐœ์„  ์ œ์•ˆ", variant="secondary")
feedback_output = gr.Textbox(label="ํ”ผ๋“œ๋ฐฑ ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ", lines=2)
with gr.Tab("๐Ÿ“ ๊ฐ์‚ฌ ๋กœ๊ทธ"):
gr.Markdown("""
### ๊ฐ์‚ฌ ์ถ”์  (Audit Trail)
**๋ชฉ์ **: ๋ชจ๋“  ์˜์‚ฌ๊ฒฐ์ • ๊ณผ์ • ์ถ”์  ๋ฐ ์ปดํ”Œ๋ผ์ด์–ธ์Šค ํ™•๋ณด
**๊ธฐ๋ก ํ•ญ๋ชฉ**:
- ๐Ÿ• ์‹œ๊ฐ„: ์ž‘์—… ์ˆ˜ํ–‰ ์‹œ๊ฐ
- ๐Ÿ‘ค ์—์ด์ „ํŠธ: MRO/๊ตฌ๋งค/๊ฒฝ์˜์ง„
- ๐Ÿ”ง ๋„๊ตฌ: ์‚ฌ์šฉํ•œ ๊ธฐ๋Šฅ
- ๐Ÿ“ฅ ์ž…๋ ฅ: ํŒŒ๋ผ๋ฏธํ„ฐ
- ๐Ÿ“ค ์ถœ๋ ฅ: ๊ฒฐ๊ณผ ์š”์•ฝ
**ํ™œ์šฉ**:
- ๊ทœ์ • ์ค€์ˆ˜ ๊ฐ์‚ฌ
- ํ”„๋กœ์„ธ์Šค ๊ฐœ์„ 
- ์ฑ…์ž„ ์ถ”์ ์„ฑ
""")
audit_table = gr.Dataframe(label="๐Ÿ“ ์ „์ฒด ๊ฐ์‚ฌ ๋กœ๊ทธ")
# Hidden outputs for optimization result
opt_result_json = gr.JSON(label="์ตœ์ ํ™” ์ƒ์„ธ ๊ฒฐ๊ณผ", visible=False)
gr.Markdown("""
---
## ๐Ÿ’ก ์‹œ์Šคํ…œ ์‚ฌ์šฉ ๊ฐ€์ด๋“œ
### ๐Ÿ“‹ ๋‹จ๊ณ„๋ณ„ ์‚ฌ์šฉ๋ฒ•
#### 1๏ธโƒฃ ์‹œ๋‚˜๋ฆฌ์˜ค ์„ ํƒ
- **๊ธด๊ธ‰ ๊ณ ์žฅ ๋Œ€์‘**: ์„ค๋น„ ๊ณ ์žฅ์œผ๋กœ ์ฆ‰์‹œ ๊ต์ฒด๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ
- **์ •๊ธฐ ๋ฐœ์ฃผ ๊ณ„ํš**: ์›”๊ฐ„/๋ถ„๊ธฐ ์ •๊ธฐ ๋ฐœ์ฃผ ์ตœ์ ํ™”
- **๊ทœ์ • ์ค€์ˆ˜ ๊ฒ€์ฆ**: ํŠน์ˆ˜ ํ’ˆ๋ชฉ ๊ตฌ๋งค ์‹œ ์ปดํ”Œ๋ผ์ด์–ธ์Šค ํ™•์ธ
#### 2๏ธโƒฃ ํŒŒ๋ผ๋ฏธํ„ฐ ์ž…๋ ฅ
- **์„ค๋น„ ID**: ๊ณ ์žฅ/์ •๋น„ ๋Œ€์ƒ ์„ค๋น„ (์ž๋™ ์ž…๋ ฅ)
- **ํ’ˆ๋ชฉ ID**: ํŠน์ • ํ’ˆ๋ชฉ ์ง€์ • (์„ ํƒ์‚ฌํ•ญ, ๋น„์šฐ๋ฉด ์ž๋™ ์„ ํƒ)
- **์ˆ˜๋Ÿ‰**: ๋ฐœ์ฃผ ํ•„์š” ์ˆ˜๋Ÿ‰
#### 3๏ธโƒฃ ๋ถ„์„ ์‹คํ–‰
- "๐Ÿš€ Composite AI ๋ถ„์„ ์‹คํ–‰" ๋ฒ„ํŠผ ํด๋ฆญ
- ์•ฝ 5-10์ดˆ ๋‚ด ๊ฒฐ๊ณผ ํ™•์ธ
#### 4๏ธโƒฃ ๊ฒฐ๊ณผ ๊ฒ€ํ† 
- **MRO ํƒญ**: ์žฌ๊ณ  ํ˜„ํ™ฉ ๋ฐ ๋ฐœ์ฃผ ํ•„์š”์„ฑ ํ™•์ธ
- **๊ตฌ๋งค ํƒญ**: ๊ณต๊ธ‰์—…์ฒด ๋น„๊ต ๋ฐ ์ตœ์  ์„ ํƒ
- **๊ฒฝ์˜์ง„ ํƒญ**: KPI ํ™•์ธ ๋ฐ ์˜์‚ฌ๊ฒฐ์ •
#### 5๏ธโƒฃ ์Šน์ธ/ํ”ผ๋“œ๋ฐฑ
- Action Items ๊ฒ€ํ†  ํ›„ ์Šน์ธ/๋ฐ˜๋ ค ๊ฒฐ์ •
- ๊ฐœ์„  ์ œ์•ˆ ์ž…๋ ฅ ์‹œ ์ž๋™์œผ๋กœ ๋‹ด๋‹น ๋ถ€์„œ์— ์ „๋‹ฌ
### ๐ŸŽ“ ํ”„๋กœ์„ธ์Šค ์ดํ•ด
๊ฐ ํƒญ์˜ "๐Ÿ“‹ ํ”„๋กœ์„ธ์Šค ๊ฐ€์ด๋“œ"๋ฅผ ํŽผ์น˜๋ฉด:
- ๋‹จ๊ณ„๋ณ„ ์ƒ์„ธ ์ ˆ์ฐจ
- ์ž…๋ ฅ/์ถœ๋ ฅ ๋ช…์„ธ
- ๋‹ด๋‹น์ž ๋ฐ ์†Œ์š” ์‹œ๊ฐ„
- ์„ฑ๊ณต ๊ธฐ์ค€
์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
### ๐Ÿ” ์ฃผ์š” ๊ธฐ๋Šฅ
1. **์ž๋™ ๋ถ€ํ’ˆ ๋งค์นญ**: ์„ค๋น„ ID๋งŒ์œผ๋กœ ํ˜ธํ™˜ ๋ถ€ํ’ˆ ์ž๋™ ๊ฒ€์ƒ‰
2. **์ „์‚ฌ ์žฌ๊ณ  ํ†ตํ•ฉ**: ๋ณธ์‚ฌ, ํฌํ•ญ, ๊ด‘์–‘ ์ „์ฒด ์ฐฝ๊ณ  ์‹ค์‹œ๊ฐ„ ์กฐํšŒ
3. **AI ๊ทœ์ • ๊ฒ€์ฆ**: ๊ทœ์ œํ’ˆ๋ชฉ, ESG ๋“ฑ๊ธ‰ ๋“ฑ ์ž๋™ ๊ฒ€์ฆ
4. **์ตœ์ ํ™” ์—”์ง„**: Linear Programming์œผ๋กœ ๋น„์šฉ ์ตœ์†Œํ™”
5. **์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ๋Œ€์‹œ๋ณด๋“œ**: Plotly ์ฐจํŠธ๋กœ ๋“œ๋ฆด๋‹ค์šด ๋ถ„์„ ๊ฐ€๋Šฅ
### ๐Ÿ”‘ API Key ์„ค์ • (Hugging Face Spaces)
OpenAI ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด:
1. Space Settings โ†’ Secrets์œผ๋กœ ์ด๋™
2. ์ƒˆ Secret ์ถ”๊ฐ€:
- Name: `OPENAI_API_KEY`
- Value: `your-openai-api-key`
3. Space ์žฌ์‹œ์ž‘
API ํ‚ค ์—†์ด๋„ ๋ฐ๋ชจ ๋ชจ๋“œ๋กœ ๊ธฐ๋ณธ ๊ธฐ๋Šฅ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
""")
# Event Handlers
scenario_radio.change(
fn=update_scenario,
inputs=[scenario_radio],
outputs=[equipment_text, item_text, demand_number, scenario_info]
)
run_button.click(
fn=run_demo,
inputs=[scenario_radio, seed_number, equipment_text, item_text, demand_number],
outputs=[
status_output, # 1
mro_inventory_plot, # 2
mro_workflow_plot, # 3
proc_comparison_plot, # 4
proc_workflow_plot, # 5
exec_kpi_plot, # 6
exec_action_table, # 7
proc_offers_table, # 8
mro_inventory_table, # 9
opt_result_json, # 10
audit_table, # 11
selected_item_display # 12
]
)
# Feedback handlers
def handle_approve(feedback):
return f"โœ… ์Šน์ธ ์™„๋ฃŒ: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\nํ”ผ๋“œ๋ฐฑ: {feedback}"
def handle_reject(feedback):
return f"โŒ ๋ฐ˜๋ ค๋จ: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n์‚ฌ์œ : {feedback}"
def handle_suggest(feedback):
return f"๐Ÿ’ก ๊ฐœ์„  ์ œ์•ˆ ์ ‘์ˆ˜: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n๋‚ด์šฉ: {feedback}"
approve_btn.click(fn=handle_approve, inputs=[feedback_text], outputs=[feedback_output])
reject_btn.click(fn=handle_reject, inputs=[feedback_text], outputs=[feedback_output])
suggest_btn.click(fn=handle_suggest, inputs=[feedback_text], outputs=[feedback_output])
print("=" * 60)
print("โœ… ํ”„๋กœ์„ธ์Šค ๊ฐ€์ด๋“œ ํ†ตํ•ฉ UI ์™„๋ฃŒ!")
print("=" * 60)
if __name__ == "__main__":
demo.launch(
server_name="0.0.0.0",
server_port=7860,
show_error=True
)
print("\n๐ŸŽ‰ ํ”„๋กœ์„ธ์Šค ๊ฐ€์ด๋“œ ํ†ตํ•ฉ ๋ฒ„์ „ ์‹คํ–‰ ์ค‘!\n")