Spaces:
Configuration error
Configuration error
| # ========================================================= | |
| # 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 | |
| # ========================================================= | |
| 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") | |