|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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λΆ" |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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": "λ°ΈλΈ/μΌμ"} |
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)) |
|
|
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 |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)}" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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" |
|
|
|
|
|
|
|
|
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": "κ·μ νλͺ© ν΄μΈκ΅¬λ§€ κΈμ§" |
|
|
}) |
|
|
|
|
|
|
|
|
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) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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] |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
print("\n" + "=" * 60) |
|
|
print("π νκΉ
νμ΄μ€ μ€νμ΄μ€μμ Gradio μλ² μμ...") |
|
|
print("=" * 60 + "\n") |
|
|
|
|
|
|
|
|
demo.launch( |
|
|
server_name="0.0.0.0", |
|
|
server_port=7860, |
|
|
share=False, |
|
|
show_error=True |
|
|
) |
|
|
|