ppp / app.py
jeongkee's picture
Upload 4 files
344ee48 verified
# =========================================================
# POSCO DX - MRO Composite AI - HUGGING FACE SPACES VERSION
# =========================================================
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
# βœ… ν—ˆκΉ…νŽ˜μ΄μŠ€ Secretsμ—μ„œ API ν‚€ λ‘œλ“œ
OPENAI_API_KEY = os.environ.get('OPENAI_API_KEY', '')
if OPENAI_API_KEY:
print("βœ… OpenAI API Key loaded from Hugging Face Secrets")
os.environ['OPENAI_API_KEY'] = OPENAI_API_KEY
else:
print("⚠️ DEMO MODE (OpenAI API Key not found)")
print("πŸ’‘ Tip: Add OPENAI_API_KEY to Hugging Face Secrets")
print("\n" + "=" * 60)
print("βœ… ν—ˆκΉ…νŽ˜μ΄μŠ€ 슀페이슀 버전 μ΄ˆκΈ°ν™” μ™„λ£Œ!")
print("=" * 60 + "\n")
# =========================================================
# 3. ν”„λ‘œμ„ΈμŠ€ κ°€μ΄λ“œ μ„€μ •
# =========================================================
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": "AI μ‹œμŠ€ν…œ (Knowledge Graph)",
"duration": "2λΆ„"
},
{
"id": "4",
"name": "전사 재고 ν˜„ν™© 확인",
"description": "λͺ¨λ“  창고의 재고λ₯Ό μ‹€μ‹œκ°„μœΌλ‘œ μ‘°νšŒν•©λ‹ˆλ‹€",
"input": "λΆ€ν’ˆ ID 리슀트",
"output": "창고별 μž¬κ³ μˆ˜λŸ‰, μ•ˆμ „μž¬κ³ , κ°€μš©μž¬κ³ ",
"owner": "AI μ‹œμŠ€ν…œ (재고 DB)",
"duration": "1λΆ„"
},
{
"id": "5",
"name": "발주 ν•„μš”μ„± νŒλ‹¨",
"description": "재고 λΆ€μ‘± μ—¬λΆ€λ₯Ό νŒλ‹¨ν•˜κ³  발주 μˆ˜λŸ‰μ„ μ‚°μΆœν•©λ‹ˆλ‹€",
"input": "κ°€μš©μž¬κ³ , μˆ˜μš”μˆ˜λŸ‰, μ•ˆμ „μž¬κ³ ",
"output": "발주 ν•„μš” μ—¬λΆ€, ꢌμž₯ λ°œμ£ΌλŸ‰",
"owner": "MROνŒ€ + AI 뢄석",
"duration": "3λΆ„"
},
{
"id": "6",
"name": "κ΅¬λ§€νŒ€ 발주 μš”μ²­",
"description": "κ΅¬λ§€νŒ€μ— 발주 μš”μ²­μ„œλ₯Ό μ „λ‹¬ν•©λ‹ˆλ‹€",
"input": "λΆ€ν’ˆλͺ…, μˆ˜λŸ‰, μš°μ„ μˆœμœ„, λ‚©κΈ°μš”κ΅¬",
"output": "λ°œμ£Όμš”μ²­λ²ˆν˜Έ",
"owner": "MROνŒ€ β†’ κ΅¬λ§€νŒ€",
"duration": "2λΆ„"
}
],
"success_criteria": [
"μ„€λΉ„ 정보 μ •ν™•νžˆ 식별",
"ν˜Έν™˜ λΆ€ν’ˆ 100% λ§€μΉ­",
"재고 ν˜„ν™© μ‹€μ‹œκ°„ 반영",
"발주 μˆ˜λŸ‰ μ΅œμ ν™”"
],
"total_duration": "μ•½ 15λΆ„"
},
"procurement": {
"title": "πŸ’° ꡬ맀/쑰달 ν”„λ‘œμ„ΈμŠ€",
"steps": [
{
"id": "1",
"name": "발주 μš”μ²­ μ ‘μˆ˜",
"description": "MROνŒ€μœΌλ‘œλΆ€ν„° 발주 μš”μ²­μ„ μ ‘μˆ˜ν•©λ‹ˆλ‹€",
"input": "λ°œμ£Όμš”μ²­λ²ˆν˜Έ, λΆ€ν’ˆμ •λ³΄, μˆ˜λŸ‰",
"output": "ꡬ맀건번호, λ‹΄λ‹Ήμž λ°°μ •",
"owner": "κ΅¬λ§€νŒ€ μ ‘μˆ˜",
"duration": "3λΆ„"
},
{
"id": "2",
"name": "곡급업체 정보 쑰회",
"description": "ν•΄λ‹Ή λΆ€ν’ˆμ„ 곡급 κ°€λŠ₯ν•œ 업체λ₯Ό μ‘°νšŒν•©λ‹ˆλ‹€",
"input": "λΆ€ν’ˆ ID, ν’ˆλͺ© μΉ΄ν…Œκ³ λ¦¬",
"output": "곡급업체 리슀트, 견적정보, ESGλ“±κΈ‰",
"owner": "AI μ‹œμŠ€ν…œ (곡급업체 DB)",
"duration": "2λΆ„"
},
{
"id": "3",
"name": "κ·œμ • μ€€μˆ˜ 검증",
"description": "Neuro-Symbolic AI둜 ꡬ맀 κ·œμ • μœ„λ°˜ μ—¬λΆ€λ₯Ό μžλ™ κ²€μ¦ν•©λ‹ˆλ‹€",
"input": "곡급업체 정보, ν’ˆλͺ© νŠΉμ„±, κ·œμ • λ£°μ…‹",
"output": "적합/뢀적합/κ²½κ³ , μœ„λ°˜ κ·œμ • λͺ©λ‘",
"owner": "AI μ‹œμŠ€ν…œ (κ·œμ • μ—”μ§„)",
"duration": "1λΆ„"
},
{
"id": "4",
"name": "졜적 λ°°λΆ„ 계산",
"description": "Linear Programming으둜 μ΅œμ €λΉ„μš© 발주 κ³„νšμ„ μˆ˜λ¦½ν•©λ‹ˆλ‹€",
"input": "곡급업체 단가, MOQ, λ¦¬λ“œνƒ€μž„, μ œμ•½μ‘°κ±΄",
"output": "업체별 λ°œμ£ΌλŸ‰, 총 λΉ„μš©, μ˜ˆμƒ μ ˆκ°μ•‘",
"owner": "AI μ‹œμŠ€ν…œ (PuLP)",
"duration": "2λΆ„"
},
{
"id": "5",
"name": "발주 μ „λž΅ 수립",
"description": "GPT-4 기반으둜 ν˜‘μƒ μ „λž΅ 및 리슀크λ₯Ό λΆ„μ„ν•©λ‹ˆλ‹€",
"input": "μ‹œμž₯ 동ν–₯, κ³Όκ±° ꡬ맀 이λ ₯, μ΅œμ ν™” κ²°κ³Ό",
"output": "ν˜‘μƒ 포인트, 리슀크 뢄석, λŒ€μ•ˆ μ‹œλ‚˜λ¦¬μ˜€",
"owner": "AI μ‹œμŠ€ν…œ (LLM)",
"duration": "5λΆ„"
},
{
"id": "6",
"name": "κ²½μ˜μ§„ 승인 μš”μ²­",
"description": "발주 κ³„νšμ„ κ²½μ˜μ§„μ—κ²Œ 승인 μš”μ²­ν•©λ‹ˆλ‹€",
"input": "발주 κ³„νšμ„œ, KPI 뢄석, λΉ„μš©νŽΈμ΅ 뢄석",
"output": "μŠΉμΈμš”μ²­λ²ˆν˜Έ",
"owner": "κ΅¬λ§€νŒ€ β†’ κ²½μ˜μ§„",
"duration": "3λΆ„"
},
{
"id": "7",
"name": "PO λ°œν–‰",
"description": "승인된 발주 κ³„νšμ— 따라 Purchase Orderλ₯Ό λ°œν–‰ν•©λ‹ˆλ‹€",
"input": "승인번호, 곡급업체 정보, 발주 상세",
"output": "PO 번호, 곡급업체 전솑 μ™„λ£Œ",
"owner": "κ΅¬λ§€νŒ€ (ERP 연동)",
"duration": "10λΆ„"
}
],
"success_criteria": [
"κ·œμ • 100% μ€€μˆ˜",
"λΉ„μš© μ΅œμ†Œν™” 달성",
"리슀크 사전 식별",
"승인 ν”„λ‘œμ„ΈμŠ€ μ™„λ£Œ"
],
"total_duration": "μ•½ 25λΆ„"
},
"executive": {
"title": "πŸ‘” κ²½μ˜μ§„ μ˜μ‚¬κ²°μ • ν”„λ‘œμ„ΈμŠ€",
"steps": [
{
"id": "1",
"name": "승인 μš”μ²­ μ•Œλ¦Ό",
"description": "κ΅¬λ§€νŒ€μœΌλ‘œλΆ€ν„° 승인 μš”μ²­μ„ μˆ˜μ‹ ν•©λ‹ˆλ‹€",
"input": "μŠΉμΈμš”μ²­λ²ˆν˜Έ, 발주 μš”μ•½",
"output": "μ•Œλ¦Ό 확인",
"owner": "μ‹œμŠ€ν…œ β†’ κ²½μ˜μ§„",
"duration": "μ¦‰μ‹œ"
},
{
"id": "2",
"name": "KPI λŒ€μ‹œλ³΄λ“œ 확인",
"description": "λΉ„μš© 절감λ₯ , μ»΄ν”ŒλΌμ΄μ–ΈμŠ€, ESG μ§€ν‘œ 등을 ν™•μΈν•©λ‹ˆλ‹€",
"input": "발주 건번호",
"output": "KPI ν˜„ν™© (λΉ„μš©/κ·œμ •/ESG/μ‹œκ°„)",
"owner": "κ²½μ˜μ§„ (λŒ€μ‹œλ³΄λ“œ)",
"duration": "2λΆ„"
},
{
"id": "3",
"name": "Action Items κ²€ν† ",
"description": "μš°μ„ μˆœμœ„λ³„ 쑰치 사항을 κ²€ν† ν•©λ‹ˆλ‹€",
"input": "Action Items 리슀트",
"output": "κ²€ν†  μ™„λ£Œ",
"owner": "κ²½μ˜μ§„",
"duration": "5λΆ„"
},
{
"id": "4",
"name": "발주 상세 뢄석",
"description": "곡급업체, 가격, 리슀크 λ“± 상세 λ‚΄μš©μ„ λΆ„μ„ν•©λ‹ˆλ‹€",
"input": "발주 κ³„νšμ„œ, AI 뢄석 리포트",
"output": "뢄석 λ©”λͺ¨",
"owner": "κ²½μ˜μ§„",
"duration": "10λΆ„"
},
{
"id": "5",
"name": "μ˜μ‚¬κ²°μ •",
"description": "승인/반렀/μ‘°κ±΄λΆ€μŠΉμΈ 쀑 ν•˜λ‚˜λ₯Ό μ„ νƒν•©λ‹ˆλ‹€",
"input": "μ’…ν•© 뢄석 κ²°κ³Ό",
"output": "승인 μ—¬λΆ€, κ²°μ • μ‚¬μœ ",
"owner": "κ²½μ˜μ§„",
"duration": "3λΆ„"
},
{
"id": "6",
"name": "ν”Όλ“œλ°± 제곡",
"description": "ν–₯ν›„ κ°œμ„ μ„ μœ„ν•œ ν”Όλ“œλ°±μ„ μž…λ ₯ν•©λ‹ˆλ‹€",
"input": "κ°œμ„  μ œμ•ˆ 사항",
"output": "ν”Όλ“œλ°± μ €μž₯, AI ν•™μŠ΅ 반영",
"owner": "κ²½μ˜μ§„",
"duration": "5λΆ„"
}
],
"success_criteria": [
"KPI λͺ©ν‘œ 달성 확인",
"리슀크 κ²€ν†  μ™„λ£Œ",
"μ‹ μ†ν•œ μ˜μ‚¬κ²°μ •",
"ν”Όλ“œλ°± 루프 ꡬ좕"
],
"total_duration": "μ•½ 25λΆ„"
}
}
# =========================================================
# 4. μ‹œλ‚˜λ¦¬μ˜€ 프리셋
# =========================================================
SCENARIO_PRESETS = {
"κΈ΄κΈ‰ κ³ μž₯ λŒ€μ‘": {
"description": "🚨 ν¬ν•­μ œμ² μ†Œ 컨베이어 베어링 κΈ΄κΈ‰ κ³ μž₯",
"equipment_id": "CONV-PH-001",
"item_id": "",
"demand_qty": 10,
"context": "컨베이어 베어링 κ³ μž₯으둜 생산라인 쀑단. μ¦‰μ‹œ ꡐ체 ν•„μš”.",
"priority": "κΈ΄κΈ‰",
"guide": "λ¦¬λ“œνƒ€μž„ μ΅œμ†Œν™” μš°μ„ . κ΅­λ‚΄ 곡급업체 μš°μ„  κ³ λ €."
},
"μ •κΈ° 발주 κ³„νš": {
"description": "πŸ“‹ μ›”κ°„ μ •κΈ° 발주 - μœ μ••νŽŒν”„ μ˜ˆλ°©μ •λΉ„",
"equipment_id": "PUMP-GY-001",
"item_id": "",
"demand_qty": 50,
"context": "μ›”κ°„ μ˜ˆλ°©μ •λΉ„ κ³„νš. 졜적 가격 및 재고 κ· ν˜• ν•„μš”.",
"priority": "정상",
"guide": "λΉ„μš© μ΅œμ ν™” μš°μ„ . ESG λ“±κΈ‰ κ³ λ €."
},
"κ·œμ • μ€€μˆ˜ 검증": {
"description": "βš–οΈ κ·œμ œν’ˆλͺ©(νŠΉμˆ˜ν™”ν•™λ¬Όμ§ˆ) ꡬ맀 검증",
"equipment_id": "VALVE-PH-001",
"item_id": "",
"demand_qty": 20,
"context": "특수 μ‹€λ§μž¬ ꡬ맀. 해외ꡬ맀 차단 κ·œμ • μ€€μˆ˜ ν•„μˆ˜.",
"priority": "κ·œμ •μ€€μˆ˜",
"guide": "μ»΄ν”ŒλΌμ΄μ–ΈμŠ€ 100% μ€€μˆ˜. κ΅­λ‚΄μ—…μ²΄λ§Œ ν—ˆμš©."
}
}
# μ‹€μ œ λΆ€ν’ˆλͺ… 및 μΉ΄ν…Œκ³ λ¦¬
REAL_PART_NAMES = {
"베어링": ["SKF 6205 볼베어링", "NSK 원톡베어링", "NTN ν…Œμ΄νΌλ² μ–΄λ§"],
"μœ€ν™œμœ ": ["μ‰˜ 였마라 220", "λͺ¨λΉŒ DTE 25", "μ§€μ—μŠ€μΉΌν…μŠ€ ν„°λΉˆμœ "],
"ν•„ν„°": ["ν•˜μ΄λ“œλ‘œλ½ μœ μ••ν•„ν„°", "파컀 에어필터", "λ„λ‚œλ“œμŠ¨ μ •λ°€ν•„ν„°"],
"벨트": ["게이츠 νŒŒμ›Œκ·Έλ¦½ 벨트", "λ°˜λ„ V벨트", "μ˜΅ν‹°λ²¨νŠΈ νƒ€μ΄λ°λ²¨νŠΈ"],
"μ„Όμ„œ": ["μ§€λ©˜μŠ€ κ·Όμ ‘μ„Όμ„œ", "μ˜€λ―€λ‘  κ΄‘μ „μ„Όμ„œ", "ν•˜λ‹ˆμ›° μ••λ ₯μ„Όμ„œ"],
"νŒ¨ν‚Ή": ["NOK 였링", "파컀 μœ μ••μ”°", "발카 κ·Έλžœλ“œνŒ¨ν‚Ή"],
"ν“¨μ¦ˆ": ["LSμ‚°μ „ MCCB", "μŠˆλ‚˜μ΄λ” 차단기", "ABB ν“¨μ¦ˆ"],
"호슀": ["파컀 μœ μ••ν˜ΈμŠ€", "만리 κ³ μ••ν˜ΈμŠ€", "λΈŒλ¦¬μ§€μŠ€ν†€ μ‚°μ—…ν˜ΈμŠ€"],
"볼트": ["SUS304 윑각볼트", "κ³ μž₯λ ₯볼트 F10T", "μ•΅μ»€λ³ΌνŠΈ M16"],
"μ‹€λ§μž¬": ["λ‘νƒ€μ΄νŠΈ μ‹€λž€νŠΈ", "μ“°λ¦¬λ³Έλ“œ μ•‘μƒνŒ¨ν‚Ή", "ν—¨μΌˆ λ°€λ΄‰μž¬"]
}
# μ‹€μ œ 곡급업체 정보
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": "밸브/μ„Όμ„œ"}
]
# =========================================================
# 5. μœ ν‹Έλ¦¬ν‹° ν•¨μˆ˜
# =========================================================
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
# =========================================================
# 6. 데이터 생성 ν•¨μˆ˜ (재고 보μž₯)
# =========================================================
def generate_demo_tables(seed: int = 7) -> Dict[str, pd.DataFrame]:
"""μΆ©λΆ„ν•œ μž¬κ³ κ°€ 보μž₯된 데λͺ¨ 데이터 생성"""
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 = []
eq_configs = [
("PUMP", "μœ μ••νŽŒν”„", ["PH", "GY"], 6),
("CONV", "컨베이어", ["PH", "GY"], 6),
("VALVE", "μ œμ–΄λ°ΈλΈŒ", ["PH", "GY"], 5),
("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 = []
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 part_name 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)
# ν˜Έν™˜μ„±
compat = []
for eq_idx, eq_row in equipment.iterrows():
eq_id = eq_row["equipment_id"]
eq_type = eq_row["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 = 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 = []
for _, item in items.iterrows():
num_storages = random.randint(1, 3)
selected_storages = storages.sample(num_storages)
for _, storage in selected_storages.iterrows():
stock_level = random.randint(50, 150) # μΆ©λΆ„ν•œ 재고
safety_stock = random.randint(10, min(30, stock_level - 10)) # stock_level보닀 μž‘κ²Œ
available = stock_level - safety_stock
reserved = random.randint(0, min(10, available)) if available > 0 else 0
inventory.append({
"storage_id": storage["storage_id"],
"item_id": item["item_id"],
"on_hand": stock_level,
"safety_stock": safety_stock,
"reserved": reserved,
"last_updated": (datetime.now() - timedelta(days=random.randint(1, 30))).strftime("%Y-%m-%d"),
})
inventory = pd.DataFrame(inventory)
# 곡급업체
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)
])
# 곡급업체 였퍼
offers = []
for _, item in items.iterrows():
num_suppliers = random.randint(3, 5)
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 = 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 = []
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
}
# =========================================================
# 7. MCP 도ꡬ λ ˆμ§€μŠ€νŠΈλ¦¬
# =========================================================
class MCPToolRegistry:
def __init__(self):
self.log = []
def call_tool(self, tool_name: str, **kwargs):
entry = {
"timestamp": now_ts(),
"tool": tool_name,
"input": kwargs,
"output": f"Tool {tool_name} executed"
}
self.log.append(entry)
return entry["output"]
def get_log(self) -> pd.DataFrame:
if not self.log:
return pd.DataFrame({"λ©”μ‹œμ§€": ["둜그 μ—†μŒ"]})
return pd.DataFrame(self.log)
# =========================================================
# 8. LLM μ˜€μΌ€μŠ€νŠΈλ ˆμ΄ν„°
# =========================================================
class LLMOrchestrator:
def __init__(self, api_key: str = ""):
self.api_key = api_key
self.demo_mode = (not api_key) or (not OPENAI_AVAILABLE)
if not self.demo_mode:
try:
self.client = OpenAI(api_key=api_key)
print("βœ… LLM: OpenAI μ—°κ²°")
except Exception as e:
print(f"⚠️ OpenAI μ—°κ²° μ‹€νŒ¨: {e}")
self.demo_mode = True
else:
print("⚠️ LLM: DEMO MODE")
def generate(self, prompt: str, temperature: float = 0.1) -> str:
if self.demo_mode:
return f"[DEMO] {prompt[:100]}에 λŒ€ν•œ AI 뢄석 κ²°κ³Ό..."
try:
response = self.client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=temperature,
max_tokens=500
)
return response.choices[0].message.content
except Exception as e:
return f"[ERROR] {str(e)}"
# =========================================================
# 9. 핡심 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직
# =========================================================
def match_equipment_items(equipment_id: str, tables: Dict) -> Tuple[Dict, pd.DataFrame]:
"""μ„€λΉ„-λΆ€ν’ˆ λ§€μΉ­"""
eq = tables["equipment"]
compat = tables["compat"]
items = tables["items"]
eq_row = eq[eq["equipment_id"] == equipment_id]
if len(eq_row) == 0:
return {}, pd.DataFrame()
eq_info = eq_row.iloc[0].to_dict()
compat_items = compat[compat["equipment_id"] == equipment_id]
compat_items = compat_items.merge(items, on="item_id", how="left")
return eq_info, compat_items
def query_inventory(item_id: str, tables: Dict) -> pd.DataFrame:
"""재고 쑰회"""
inv = tables["inventory"]
storages = tables["storages"]
item_inv = inv[inv["item_id"] == item_id].copy()
if len(item_inv) == 0:
return pd.DataFrame()
item_inv = item_inv.merge(storages, on="storage_id", how="left")
item_inv["available"] = item_inv["on_hand"] - item_inv["reserved"]
return item_inv[["storage_name", "on_hand", "safety_stock", "reserved", "available", "last_updated"]]
def apply_procurement_rules(offers_df: pd.DataFrame, item_info: Dict, tables: Dict) -> Dict:
"""κ·œμ • 검증"""
rules_result = {
"passed": [],
"warnings": [],
"blocked": []
}
if "supplier_type" not in offers_df.columns:
offers_df["supplier_type"] = "κ΅­λ‚΄"
if "supplier_name" not in offers_df.columns:
offers_df["supplier_name"] = "Unknown"
if "esg_level" not in offers_df.columns:
offers_df["esg_level"] = "B"
# R-001: κ·œμ œν’ˆλͺ© 해외ꡬ맀 μ œν•œ
if item_info.get("risk_class") == "규제":
blocked_mask = offers_df["supplier_type"] == "ν•΄μ™Έ"
blocked_suppliers = offers_df[blocked_mask]["supplier_name"].tolist()
if blocked_suppliers:
rules_result["blocked"].append({
"rule": "R-001",
"suppliers": blocked_suppliers,
"reason": "κ·œμ œν’ˆλͺ© 해외ꡬ맀 κΈˆμ§€"
})
# R-004: ESG Cλ“±κΈ‰ μ œν•œ
c_grade_mask = offers_df["esg_level"] == "C"
c_grade_suppliers = offers_df[c_grade_mask]["supplier_name"].tolist()
if c_grade_suppliers:
rules_result["warnings"].append({
"rule": "R-004",
"suppliers": c_grade_suppliers,
"reason": "ESG Cλ“±κΈ‰ νŒ¨λ„ν‹°"
})
return rules_result
def optimize_procurement(offers_df: pd.DataFrame, demand_qty: int, rules_eval: Dict) -> Dict:
"""ꡬ맀 μ΅œμ ν™”"""
if not PULP_AVAILABLE:
return {
"status": "UNAVAILABLE",
"message": "PuLP not installed",
"allocation": {}
}
blocked_suppliers = []
for block in rules_eval.get("blocked", []):
blocked_suppliers.extend(block.get("suppliers", []))
if "supplier_name" in offers_df.columns:
valid_offers = offers_df[~offers_df["supplier_name"].isin(blocked_suppliers)].copy()
else:
valid_offers = offers_df.copy()
if len(valid_offers) == 0:
return {"status": "NO_VALID_SUPPLIERS"}
try:
prob = LpProblem("Procurement_Optimization", LpMinimize)
vars_dict = {}
for idx, row in valid_offers.iterrows():
var = LpVariable(f"qty_{row['supplier_id']}", lowBound=0, cat='Integer')
vars_dict[idx] = var
prob += lpSum([valid_offers.loc[idx, "unit_price"] * vars_dict[idx] for idx in vars_dict])
prob += lpSum([vars_dict[idx] for idx in vars_dict]) >= demand_qty
prob.solve()
allocation = {}
total_cost = 0
for idx, var in vars_dict.items():
qty = var.varValue
if qty and qty > 0:
row = valid_offers.loc[idx]
allocation[row["supplier_id"]] = {
"qty": int(qty),
"unit_price": int(row["unit_price"]),
"total": int(qty * row["unit_price"])
}
total_cost += int(qty * row["unit_price"])
return {
"status": LpStatus[prob.status],
"total_cost": total_cost,
"allocation": allocation,
"savings_pct": round(random.uniform(5, 25), 1)
}
except Exception as e:
return {
"status": "ERROR",
"message": str(e)
}
# =========================================================
# 10. μ—μ΄μ „νŠΈ ν•¨μˆ˜
# =========================================================
def validation_agent(state: Dict) -> Dict:
"""μ—μ΄μ „νŠΈ 1: 데이터 검증"""
print("πŸ“‹ μ—μ΄μ „νŠΈ 1: 데이터 검증 μ‹œμž‘")
tables = state["tables"]
mcp = state["mcp"]
equipment_id = state["equipment_id"]
item_id = state.get("item_id", "")
mcp.call_tool("match_equipment_items", equipment_id=equipment_id)
equipment_info, compat_items = match_equipment_items(equipment_id, tables)
if not equipment_info:
state["progress"] = "1/4 검증 μ‹€νŒ¨"
state["equipment_info"] = {}
state["compat_items"] = pd.DataFrame()
return state
if not item_id and len(compat_items) > 0:
mandatory = compat_items[compat_items["is_mandatory"] == True]
if len(mandatory) > 0:
selected = mandatory.iloc[0]
else:
selected = compat_items.iloc[0]
item_id = selected["item_id"]
state["item_id"] = item_id
state["selected_item_name"] = selected["item_name"]
else:
items = tables["items"]
item_row = items[items["item_id"] == item_id]
if len(item_row) > 0:
state["selected_item_name"] = item_row.iloc[0]["item_name"]
state["equipment_info"] = equipment_info
state["compat_items"] = compat_items
state["progress"] = "1/4 검증 μ™„λ£Œ"
print(f"βœ… 검증 μ™„λ£Œ: {equipment_info.get('equipment_name', 'N/A')}")
return state
def mro_agent(state: Dict) -> Dict:
"""μ—μ΄μ „νŠΈ 2: MRO 뢄석"""
print("πŸ”§ μ—μ΄μ „νŠΈ 2: MRO 뢄석 μ‹œμž‘")
tables = state["tables"]
mcp = state["mcp"]
llm = state["llm"]
item_id = state["item_id"]
mcp.call_tool("query_inventory", item_id=item_id)
inventory_view = query_inventory(item_id, tables)
state["inventory_view"] = inventory_view
state["progress"] = "2/4 MRO μ™„λ£Œ"
if not llm.demo_mode:
prompt = f"재고 뢄석: {state.get('selected_item_name', 'N/A')}, μˆ˜μš” {state['demand_qty']}개"
narrative = llm.generate(prompt, temperature=0.1)
state["narrative"] = narrative
print(f"βœ… MRO 뢄석 μ™„λ£Œ: 재고 {len(inventory_view)}개 μ°½κ³ ")
return state
def procurement_agent(state: Dict) -> Dict:
"""μ—μ΄μ „νŠΈ 3: ꡬ맀 뢄석"""
print("πŸ’° μ—μ΄μ „νŠΈ 3: ꡬ맀 뢄석 μ‹œμž‘")
tables = state["tables"]
mcp = state["mcp"]
item_id = state["item_id"]
demand_qty = state["demand_qty"]
offers = tables["supplier_offers"]
item_offers = offers[offers["item_id"] == item_id].copy()
suppliers = tables["suppliers"]
if len(item_offers) > 0:
item_offers = item_offers.merge(suppliers, on="supplier_id", how="left")
if "supplier_name" not in item_offers.columns:
item_offers["supplier_name"] = "Unknown"
if "esg_level" not in item_offers.columns:
item_offers["esg_level"] = "B"
if "supplier_type" not in item_offers.columns:
item_offers["supplier_type"] = "κ΅­λ‚΄"
items = tables["items"]
item_row = items[items["item_id"] == item_id]
if len(item_row) > 0:
item_info = item_row.iloc[0].to_dict()
else:
item_info = {"risk_class": "일반"}
mcp.call_tool("apply_rules", item_id=item_id)
rules_eval = apply_procurement_rules(item_offers, item_info, tables)
mcp.call_tool("optimize", demand_qty=demand_qty)
optimization = optimize_procurement(item_offers, demand_qty, rules_eval)
state["offers_view"] = item_offers
state["rules_eval"] = rules_eval
state["optimization"] = optimization
state["progress"] = "3/4 ꡬ맀 μ™„λ£Œ"
print(f"βœ… ꡬ맀 뢄석 μ™„λ£Œ: {len(item_offers)}개 곡급업체")
return state
def executive_agent(state: Dict) -> Dict:
"""μ—μ΄μ „νŠΈ 4: κ²½μ˜μ§„ μ˜μ‚¬κ²°μ • 지원"""
print("πŸ‘” μ—μ΄μ „νŠΈ 4: κ²½μ˜μ§„ μ˜μ‚¬κ²°μ • 지원 μ‹œμž‘")
mcp = state["mcp"]
audit_log = mcp.get_log()
state["audit_log"] = audit_log
state["progress"] = "4/4 μ™„λ£Œ βœ“"
print("βœ… κ²½μ˜μ§„ μ˜μ‚¬κ²°μ • 지원 μ™„λ£Œ")
return state
# =========================================================
# 11. μ›Œν¬ν”Œλ‘œμš° μ‹€ν–‰
# =========================================================
def run_composite_ai_workflow(
scenario: str,
equipment_id: str,
item_id: str,
demand_qty: int,
seed: int = 7
) -> Dict:
"""Composite AI μ›Œν¬ν”Œλ‘œμš° μ‹€ν–‰"""
print(f"\n{'='*60}")
print(f"πŸš€ Composite AI μ›Œν¬ν”Œλ‘œμš° μ‹œμž‘")
print(f"{'='*60}\n")
tables = generate_demo_tables(seed)
state = {
"tables": tables,
"mcp": MCPToolRegistry(),
"llm": LLMOrchestrator(OPENAI_API_KEY),
"scenario": scenario,
"equipment_id": equipment_id,
"item_id": item_id,
"demand_qty": demand_qty,
"progress": "0/4 μ‹œμž‘",
"selected_item_name": "N/A"
}
state = validation_agent(state)
state = mro_agent(state)
state = procurement_agent(state)
state = executive_agent(state)
print(f"\n{'='*60}")
print(f"βœ… Composite AI μ›Œν¬ν”Œλ‘œμš° μ™„λ£Œ")
print(f"{'='*60}\n")
return state
# =========================================================
# 12. Plotly μ‹œκ°ν™” ν•¨μˆ˜λ“€
# =========================================================
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="재고 데이터 μ—†μŒ",
xref="paper", yref="paper",
x=0.5, y=0.5,
showarrow=False,
font=dict(size=20, color="gray")
)
return fig
fig = make_subplots(
rows=2, cols=2,
subplot_titles=("창고별 재고 ν˜„ν™©", "총 재고 κ²Œμ΄μ§€", "재고 μƒνƒœ 뢄포", "상세 데이터"),
specs=[
[{"type": "bar"}, {"type": "indicator"}],
[{"type": "pie"}, {"type": "table"}]
],
vertical_spacing=0.12,
horizontal_spacing=0.15
)
fig.add_trace(
go.Bar(
x=inv_df["storage_name"],
y=inv_df["on_hand"],
name="ν˜„μž¬κ³ ",
marker_color="#2196F3"
),
row=1, col=1
)
fig.add_trace(
go.Bar(
x=inv_df["storage_name"],
y=inv_df["safety_stock"],
name="μ•ˆμ „μž¬κ³ ",
marker_color="#FFC107"
),
row=1, col=1
)
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,
title={"text": f"총 μž¬κ³ λŸ‰<br>{item_name}"},
delta={"reference": total_safety, "increasing": {"color": "green"}},
gauge={
"axis": {"range": [0, max(total_stock * 1.2, total_safety * 2)]},
"bar": {"color": "#4CAF50"},
"threshold": {
"line": {"color": "red", "width": 4},
"thickness": 0.75,
"value": total_safety
}
}
),
row=1, col=2
)
total_available = (inv_df["on_hand"] - inv_df["reserved"]).sum()
total_reserved = inv_df["reserved"].sum()
fig.add_trace(
go.Pie(
labels=["κ°€μš©μž¬κ³ ", "μ˜ˆμ•½λ¨", "μ•ˆμ „μž¬κ³ "],
values=[total_available, total_reserved, total_safety],
hole=0.3,
marker=dict(colors=["#4CAF50", "#FF9800", "#F44336"])
),
row=2, col=1
)
fig.add_trace(
go.Table(
header=dict(
values=["μ°½κ³ ", "ν˜„μž¬κ³ ", "μ•ˆμ „μž¬κ³ ", "μ˜ˆμ•½", "κ°€μš©"],
fill_color="#2196F3",
font=dict(color="white", size=12),
align="center"
),
cells=dict(
values=[
inv_df["storage_name"],
inv_df["on_hand"],
inv_df["safety_stock"],
inv_df["reserved"],
inv_df["available"]
],
fill_color="white",
align="center"
)
),
row=2, col=2
)
fig.update_layout(
title_text=f"πŸ“¦ MRO 재고 뢄석 λŒ€μ‹œλ³΄λ“œ",
height=800,
showlegend=True
)
return fig
def create_mro_workflow_status(equipment_info: Dict, compat_items: pd.DataFrame) -> go.Figure:
"""MRO μ›Œν¬ν”Œλ‘œμš° μ§„ν–‰ μƒνƒœ"""
fig = go.Figure()
if not equipment_info:
fig.add_annotation(text="μ„€λΉ„ 정보 μ—†μŒ", showarrow=False)
return fig
steps = ["κ³ μž₯ μ ‘μˆ˜", "λΆ€ν’ˆ 확인", "재고 확인", "발주 μš”μ²­"]
values = [100, 80, 60, 40]
fig.add_trace(go.Funnel(
y=steps,
x=values,
textinfo="label+percent initial",
marker=dict(color=["#2196F3", "#4CAF50", "#FF9800", "#F44336"])
))
fig.update_layout(
title=f"πŸ”„ MRO μ›Œν¬ν”Œλ‘œμš° μ§„ν–‰: {equipment_info.get('equipment_name', 'N/A')}",
height=400
)
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)
return fig
required_columns = {
'supplier_name': 'Unknown',
'unit_price': 0,
'lead_time_days': 0,
'esg_level': 'B'
}
for col, default_val in required_columns.items():
if col not in offers_df.columns:
offers_df[col] = default_val
fig = make_subplots(
rows=2, cols=2,
subplot_titles=("가격 비ꡐ", "가격-λ‚©κΈ° 뢄석", "ESG λ“±κΈ‰ 뢄포", "μ’…ν•© 평가"),
specs=[
[{"type": "bar"}, {"type": "scatter"}],
[{"type": "pie"}, {"type": "table"}]
],
vertical_spacing=0.12,
horizontal_spacing=0.15
)
blocked_suppliers = []
for block in rules_eval.get("blocked", []):
blocked_suppliers.extend(block.get("suppliers", []))
if "supplier_name" in offers_df.columns:
offers_df["blocked"] = offers_df["supplier_name"].isin(blocked_suppliers)
else:
offers_df["blocked"] = False
colors = ["red" if b else "green" for b in offers_df["blocked"]]
fig.add_trace(
go.Bar(
x=offers_df["supplier_name"],
y=offers_df["unit_price"],
marker_color=colors,
showlegend=False
),
row=1, col=1
)
fig.add_trace(
go.Scatter(
x=offers_df["lead_time_days"],
y=offers_df["unit_price"],
mode="markers+text",
text=offers_df["supplier_name"],
textposition="top center",
marker=dict(
size=15,
color=["red" if b else "green" for b in offers_df["blocked"]],
line=dict(width=2, color="black")
),
showlegend=False
),
row=1, col=2
)
try:
esg_counts = offers_df["esg_level"].value_counts()
fig.add_trace(
go.Pie(
labels=esg_counts.index,
values=esg_counts.values,
hole=0.3,
marker=dict(colors=["#4CAF50", "#FF9800", "#F44336"])
),
row=2, col=1
)
except:
fig.add_trace(
go.Pie(
labels=["N/A"],
values=[1],
hole=0.3,
marker=dict(colors=["#CCCCCC"])
),
row=2, col=1
)
max_price = offers_df["unit_price"].max() if offers_df["unit_price"].max() > 0 else 1
max_lead = offers_df["lead_time_days"].max() if offers_df["lead_time_days"].max() > 0 else 1
offers_df["price_score"] = 100 - (offers_df["unit_price"] / max_price * 50)
offers_df["lead_score"] = 100 - (offers_df["lead_time_days"] / max_lead * 30)
esg_map = {"A": 20, "B": 10, "C": 0}
offers_df["esg_score"] = offers_df["esg_level"].map(lambda x: esg_map.get(x, 10))
offers_df["total_score"] = offers_df["price_score"] + offers_df["lead_score"] + offers_df["esg_score"]
offers_sorted = offers_df.sort_values("total_score", ascending=False)
fig.add_trace(
go.Table(
header=dict(
values=["μˆœμœ„", "곡급업체", "단가", "λ‚©κΈ°", "ESG", "점수"],
fill_color="#2196F3",
font=dict(color="white", size=12),
align="center"
),
cells=dict(
values=[
list(range(1, len(offers_sorted)+1)),
offers_sorted["supplier_name"],
offers_sorted["unit_price"],
offers_sorted["lead_time_days"].astype(str) + "일",
offers_sorted["esg_level"],
offers_sorted["total_score"].round(1)
],
fill_color=[["white" if not b else "#ffcccb" for b in offers_sorted["blocked"]]],
align="center"
)
),
row=2, col=2
)
fig.update_layout(
title_text="πŸ“Š 곡급업체 μ’…ν•© 비ꡐ λŒ€μ‹œλ³΄λ“œ",
height=800,
showlegend=False
)
return fig
def create_procurement_workflow(opt_result: Dict) -> go.Figure:
"""ꡬ맀 μ›Œν¬ν”Œλ‘œμš°"""
fig = go.Figure()
steps = ["μš”μ²­ μ ‘μˆ˜", "업체 쑰회", "κ·œμ • 검증", "μ΅œμ ν™”", "승인 μš”μ²­"]
values = [100, 90, 75, 60, 40]
fig.add_trace(go.Funnel(
y=steps,
x=values,
textinfo="label+percent initial",
marker=dict(color=["#2196F3", "#4CAF50", "#FF9800", "#9C27B0", "#F44336"])
))
fig.update_layout(
title=f"πŸ”„ ꡬ맀 μ›Œν¬ν”Œλ‘œμš° (μ΅œμ ν™”: {opt_result.get('status', 'N/A')})",
height=400
)
return fig
def create_executive_kpi_dashboard(opt_result: Dict, offers_df: pd.DataFrame, history_df: 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"}]
],
vertical_spacing=0.2,
horizontal_spacing=0.1
)
savings = opt_result.get("savings_pct", 18.5)
fig.add_trace(
go.Indicator(
mode="gauge+number+delta",
value=savings,
title={"text": "λΉ„μš© 절감λ₯  (%)"},
delta={"reference": 15, "increasing": {"color": "green"}},
gauge={
"axis": {"range": [0, 30]},
"bar": {"color": "#4CAF50"},
"threshold": {"line": {"color": "red", "width": 4}, "thickness": 0.75, "value": 15}
}
),
row=1, col=1
)
fig.add_trace(
go.Indicator(
mode="gauge+number",
value=100,
title={"text": "κ·œμ • μ€€μˆ˜μœ¨ (%)"},
gauge={
"axis": {"range": [80, 100]},
"bar": {"color": "#2196F3"},
"steps": [
{"range": [80, 95], "color": "#FFE082"},
{"range": [95, 100], "color": "#C8E6C9"}
]
}
),
row=1, col=2
)
if len(offers_df) > 0 and "esg_level" in offers_df.columns:
esg_map = {"A": 100, "B": 70, "C": 40}
try:
avg_esg = offers_df["esg_level"].map(esg_map).mean()
if pd.isna(avg_esg):
avg_esg = 85
except:
avg_esg = 85
else:
avg_esg = 85
fig.add_trace(
go.Indicator(
mode="gauge+number",
value=avg_esg,
title={"text": "ESG 점수"},
gauge={
"axis": {"range": [0, 100]},
"bar": {"color": "#8BC34A"}
}
),
row=1, col=3
)
fig.add_trace(
go.Indicator(
mode="number+delta",
value=20,
delta={"reference": 30, "decreasing": {"color": "green"}},
title={"text": "처리 μ‹œκ°„ (λΆ„)"}
),
row=2, col=1
)
fig.add_trace(
go.Indicator(
mode="number+delta",
value=92,
delta={"reference": 85, "increasing": {"color": "green"}},
title={"text": "KPI 달성λ₯  (%)"}
),
row=2, col=2
)
months = ["1μ›”", "2μ›”", "3μ›”", "4μ›”", "5μ›”", "6μ›”"]
efficiency = [70, 75, 80, 85, 90, 92]
fig.add_trace(
go.Scatter(
x=months,
y=efficiency,
mode="lines+markers",
line=dict(color="#2196F3", width=3),
marker=dict(size=10),
name="νš¨μœ¨μ„±"
),
row=2, col=3
)
fig.update_layout(
title_text="πŸ“Š κ²½μ˜μ§„ KPI λŒ€μ‹œλ³΄λ“œ",
height=600,
showlegend=False
)
return fig
def create_action_items_table(opt_result: Dict, offers_df: pd.DataFrame) -> pd.DataFrame:
"""Action Items ν…Œμ΄λΈ” 생성"""
items = []
if opt_result.get("status") == "Optimal":
savings = opt_result.get("savings_pct", 0)
if savings > 20:
items.append({
"μš°μ„ μˆœμœ„": "🟒 보톡",
"쑰치 사항": "λΉ„μš© 절감 λͺ©ν‘œ 초과 달성",
"λ‹΄λ‹Ή": "κ΅¬λ§€νŒ€",
"κΈ°ν•œ": "1주일"
})
elif savings > 10:
items.append({
"μš°μ„ μˆœμœ„": "🟑 μ€‘μš”",
"쑰치 사항": "λΉ„μš© 절감 λͺ©ν‘œ 달성",
"λ‹΄λ‹Ή": "κ΅¬λ§€νŒ€",
"κΈ°ν•œ": "3일"
})
if len(offers_df) > 0:
fastest = offers_df.nsmallest(1, "lead_time_days").iloc[0]
items.append({
"μš°μ„ μˆœμœ„": "πŸ”΄ κΈ΄κΈ‰",
"쑰치 사항": f"{fastest['supplier_name']} 발주 승인 ν•„μš”",
"λ‹΄λ‹Ή": "κ²½μ˜μ§„",
"κΈ°ν•œ": "μ¦‰μ‹œ"
})
if not items:
items.append({
"μš°μ„ μˆœμœ„": "🟒 보톡",
"쑰치 사항": "정상 처리 μ§„ν–‰ 쀑",
"λ‹΄λ‹Ή": "MROνŒ€",
"κΈ°ν•œ": "1주일"
})
return pd.DataFrame(items)
# =========================================================
# 13. 메인 μ‹€ν–‰ ν•¨μˆ˜
# =========================================================
def run_analysis(
scenario: str,
equipment_dropdown: str,
item_dropdown: str,
demand_qty: int
) -> Tuple:
"""뢄석 μ‹€ν–‰"""
try:
equipment_id = equipment_dropdown.split(" - ")[0] if equipment_dropdown else ""
item_id = item_dropdown.split(" - ")[0] if " - " in item_dropdown else ""
if not equipment_id:
raise ValueError("μ„€λΉ„λ₯Ό μ„ νƒν•΄μ£Όμ„Έμš”")
if demand_qty <= 0:
raise ValueError("μˆ˜λŸ‰μ€ 1 이상이어야 ν•©λ‹ˆλ‹€")
out = run_composite_ai_workflow(
scenario=scenario,
equipment_id=equipment_id,
item_id=item_id,
demand_qty=int(demand_qty),
seed=7
)
tables = out["tables"]
equipment_info = out.get("equipment_info", {})
item_name = out.get("selected_item_name", "N/A")
mode = "βœ… LLM" if (OPENAI_API_KEY and not out["llm"].demo_mode) else "⚠️ DEMO"
status_dict = {
"mode": mode,
"scenario": scenario,
"equipment": equipment_info.get("equipment_name", equipment_id),
"item_name": item_name,
"demand": demand_qty,
"priority": SCENARIO_PRESETS.get(scenario, {}).get("priority", "정상"),
"tables_ok": True,
"progress": out["progress"]
}
status_text = format_status(status_dict)
inv_df = out.get("inventory_view", pd.DataFrame())
offers_df = out.get("offers_view", pd.DataFrame())
audit_df = out.get("audit_log", pd.DataFrame())
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())
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)
if len(audit_df) == 0:
audit_df = pd.DataFrame({"λ©”μ‹œμ§€": ["κ°μ‚¬λ‘œκ·Έ μ—†μŒ"]})
print("βœ… λŒ€μ‹œλ³΄λ“œ 생성 μ™„λ£Œ\n")
return (
status_text,
mro_dashboard,
mro_workflow,
proc_dashboard,
proc_workflow,
exec_dashboard,
action_items,
offers_df,
inv_df,
opt_result,
audit_df,
item_name
)
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', '')}
"""
equipment_value = preset["equipment_id"]
item_value = preset["item_id"] if preset["item_id"] else ""
return (
equipment_value,
item_value,
preset["demand_qty"],
guide_text
)
# =========================================================
# 14. Gradio UI (ν—ˆκΉ…νŽ˜μ΄μŠ€ μ΅œμ ν™”)
# =========================================================
print("🎨 Gradio UI ꡬ성 쀑...\n")
# λ“œλ‘­λ‹€μš΄ μ˜΅μ…˜ 생성
demo_tables = generate_demo_tables(7)
equipment_options = []
for _, row in demo_tables["equipment"].iterrows():
equipment_options.append(f"{row['equipment_id']} - {row['equipment_name']}")
item_options = ["(μžλ™ 선택)"]
for _, row in demo_tables["items"].iterrows():
item_options.append(f"{row['item_id']} - {row['item_name']}")
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 운영 β†’ ꡬ맀/쑰달 β†’ κ²½μ˜μ§„ 승인
### ✨ νŠΉμ§•
- βœ… Knowledge Graph 기반 μ„€λΉ„-λΆ€ν’ˆ λ§€μΉ­
- βœ… Neuro-Symbolic AI κ·œμ • 검증
- βœ… Linear Programming λΉ„μš© μ΅œμ ν™”
- βœ… GPT-4 기반 μ „λž΅ 뢄석
- βœ… μ‹€μ‹œκ°„ λŒ€μ‹œλ³΄λ“œ
### πŸ”‘ API ν‚€ μ„€μ •
OpenAI API ν‚€λ₯Ό μ‚¬μš©ν•˜λ €λ©΄ Hugging Face Space의 Settings β†’ Secretsμ—μ„œ `OPENAI_API_KEY`λ₯Ό μΆ”κ°€ν•˜μ„Έμš”.
""")
with gr.Accordion("πŸ“– 전체 업무 ν”„λ‘œμ„ΈμŠ€ κ°œμš”", open=False):
gr.Markdown("""
### πŸ”„ End-to-End μ›Œν¬ν”Œλ‘œμš°
```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 1️⃣ MRO 운영 β”‚ ───> β”‚ 2️⃣ ꡬ맀/쑰달 β”‚ ───> β”‚ 3️⃣ κ²½μ˜μ§„ 승인 β”‚
β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β€’ κ³ μž₯ μ ‘μˆ˜ β”‚ β”‚ β€’ 곡급업체 쑰회 β”‚ β”‚ β€’ KPI 확인 β”‚
β”‚ β€’ λΆ€ν’ˆ 확인 β”‚ β”‚ β€’ κ·œμ • 검증 β”‚ β”‚ β€’ μ˜μ‚¬κ²°μ • β”‚
β”‚ β€’ 재고 확인 β”‚ β”‚ β€’ μ΅œμ ν™” 뢄석 β”‚ β”‚ β€’ ν”Όλ“œλ°± β”‚
β”‚ β€’ 발주 μš”μ²­ β”‚ β”‚ β€’ 승인 μš”μ²­ β”‚ β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
⏱️ 15λΆ„ ⏱️ 25λΆ„ ⏱️ 25λΆ„
```
### πŸ’‘ 핡심 κ°€μΉ˜
1. **μžλ™ν™”**: μ„€λΉ„-λΆ€ν’ˆ λ§€μΉ­, 재고 쑰회, κ·œμ • 검증
2. **μ΅œμ ν™”**: AI 기반 λΉ„μš© μ΅œμ ν™”
3. **검증**: 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("### βš™οΈ νŒŒλΌλ―Έν„°")
equipment_dropdown = gr.Dropdown(
choices=equipment_options,
value=equipment_options[0],
label="μ„€λΉ„ 선택 (ID - λͺ…μΉ­)",
info="μ„€λΉ„λ₯Ό μ„ νƒν•˜μ„Έμš”"
)
item_dropdown = gr.Dropdown(
choices=item_options,
value=item_options[0],
label="ν’ˆλͺ© 선택 (ID - λͺ…μΉ­)",
info="λΉ„μ›Œλ‘λ©΄ μžλ™ 선택"
)
demand_number = gr.Number(value=10, label="μˆ˜λŸ‰", precision=0, minimum=1)
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 λ‹΄λ‹Ήμž"):
with gr.Accordion("πŸ“‹ MRO 운영 ν”„λ‘œμ„ΈμŠ€ κ°€μ΄λ“œ", open=True):
gr.HTML(create_process_guide_html("mro"))
gr.Markdown("### πŸ“Š λŒ€μ‹œλ³΄λ“œ 및 뢄석 κ²°κ³Ό")
mro_dashboard_plot = gr.Plot(label="πŸ“¦ 재고 뢄석 λŒ€μ‹œλ³΄λ“œ")
mro_workflow_plot = gr.Plot(label="πŸ”„ MRO μ›Œν¬ν”Œλ‘œμš° μ§„ν–‰")
mro_inventory_table = gr.Dataframe(label="πŸ“‹ 상세 재고 데이터")
with gr.Tab("πŸ’° ꡬ맀 λ‹΄λ‹Ήμž"):
with gr.Accordion("πŸ“‹ ꡬ맀/쑰달 ν”„λ‘œμ„ΈμŠ€ κ°€μ΄λ“œ", open=True):
gr.HTML(create_process_guide_html("procurement"))
gr.Markdown("### πŸ“Š λŒ€μ‹œλ³΄λ“œ 및 뢄석 κ²°κ³Ό")
proc_dashboard_plot = gr.Plot(label="πŸ“Š 곡급업체 비ꡐ λŒ€μ‹œλ³΄λ“œ")
proc_workflow_plot = gr.Plot(label="πŸ”„ ꡬ맀 μ›Œν¬ν”Œλ‘œμš°")
proc_offers_table = gr.Dataframe(label="πŸ“‹ 곡급업체 상세 정보")
with gr.Tab("πŸ‘” κ²½μ˜μ§„"):
with gr.Accordion("πŸ“‹ κ²½μ˜μ§„ μ˜μ‚¬κ²°μ • ν”„λ‘œμ„ΈμŠ€ κ°€μ΄λ“œ", open=True):
gr.HTML(create_process_guide_html("executive"))
gr.Markdown("### πŸ“Š λŒ€μ‹œλ³΄λ“œ 및 뢄석 κ²°κ³Ό")
exec_dashboard_plot = gr.Plot(label="πŸ“Š κ²½μ˜μ§„ KPI λŒ€μ‹œλ³΄λ“œ")
exec_action_items_table = gr.Dataframe(label="πŸ“‹ Action Items")
gr.Markdown("### πŸ’¬ κ²½μ˜μ§„ ν”Όλ“œλ°±")
feedback_text = gr.Textbox(label="κ°œμ„  μ œμ•ˆ / ν”Όλ“œλ°±", 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="ν”Όλ“œλ°± 처리 κ²°κ³Ό", interactive=False)
with gr.Tab("πŸ“ 감사 둜그"):
gr.Markdown("""
### πŸ“ 감사 좔적
λͺ¨λ“  AI μ˜μ‚¬κ²°μ • κ³Όμ •κ³Ό 도ꡬ 호좜 이λ ₯을 μΆ”μ ν•©λ‹ˆλ‹€.
""")
audit_log_table = gr.Dataframe(label="πŸ“ 전체 감사 둜그")
opt_result_display = gr.JSON(label="πŸ” μ΅œμ ν™” 상세 κ²°κ³Ό")
# 이벀트 ν•Έλ“€λŸ¬
scenario_radio.change(
fn=update_scenario,
inputs=[scenario_radio],
outputs=[equipment_dropdown, item_dropdown, demand_number, scenario_info]
)
run_button.click(
fn=run_analysis,
inputs=[scenario_radio, equipment_dropdown, item_dropdown, demand_number],
outputs=[
status_output,
mro_dashboard_plot,
mro_workflow_plot,
proc_dashboard_plot,
proc_workflow_plot,
exec_dashboard_plot,
exec_action_items_table,
proc_offers_table,
mro_inventory_table,
opt_result_display,
audit_log_table,
selected_item_display
]
)
def process_feedback(feedback_text: str, action: str) -> str:
timestamp = now_ts()
return f"βœ… {action} μ™„λ£Œ: {timestamp}\nν”Όλ“œλ°±: {feedback_text if feedback_text else '(μ—†μŒ)'}"
approve_btn.click(
fn=lambda fb: process_feedback(fb, "승인"),
inputs=[feedback_text],
outputs=[feedback_output]
)
reject_btn.click(
fn=lambda fb: process_feedback(fb, "반렀"),
inputs=[feedback_text],
outputs=[feedback_output]
)
suggest_btn.click(
fn=lambda fb: process_feedback(fb, "κ°œμ„  μ œμ•ˆ"),
inputs=[feedback_text],
outputs=[feedback_output]
)
# =========================================================
# 15. ν—ˆκΉ…νŽ˜μ΄μŠ€ 슀페이슀 μ‹€ν–‰
# =========================================================
if __name__ == "__main__":
print("\n" + "=" * 60)
print("πŸš€ ν—ˆκΉ…νŽ˜μ΄μŠ€ μŠ€νŽ˜μ΄μŠ€μ—μ„œ Gradio μ„œλ²„ μ‹œμž‘...")
print("=" * 60 + "\n")
# βœ… ν—ˆκΉ…νŽ˜μ΄μŠ€ 슀페이슀 μ „μš© μ„€μ •
demo.launch(
server_name="0.0.0.0", # λͺ¨λ“  IPμ—μ„œ μ ‘κ·Ό ν—ˆμš©
server_port=7860, # ν—ˆκΉ…νŽ˜μ΄μŠ€ κΈ°λ³Έ 포트
share=False, # ν—ˆκΉ…νŽ˜μ΄μŠ€λŠ” 자체 URL 제곡
show_error=True
)