initial_state
Field Type = JSON Code
Required = true
breaking_news
Field Type = Short text
Required = false
state → initial_state
import random
def main(state: dict) -> dict:
# ==========================================
# 1. ПРОИЗВОДСТВО СЫРЬЯ (Заводы-поставщики)
# ==========================================
# Учалы (Базальтовая крошка)
if state['factory_uchaly_basalt']['status'] != 'maintenance':
prod = state['factory_uchaly_basalt']['current_output'] * random.uniform(0.9, 1.1)
state['factory_uchaly_basalt']['ready_to_ship_stock'] += prod
if random.random() < 0.05: state['factory_uchaly_basalt']['status'] = 'maintenance'
else:
if random.random() < 0.2: state['factory_uchaly_basalt']['status'] = 'normal'
# Воскресенск (Стеклохолст)
if state['factory_voskresensk_fiberglass']['status'] != 'maintenance':
prod = state['factory_voskresensk_fiberglass']['current_output'] * random.uniform(0.9, 1.1)
state['factory_voskresensk_fiberglass']['ready_to_ship_stock'] += prod
if random.random() < 0.05: state['factory_voskresensk_fiberglass']['status'] = 'maintenance'
else:
if random.random() < 0.2: state['factory_voskresensk_fiberglass']['status'] = 'normal'
# Рязань (Битум)
if state['factory_ryazan_bitumen']['status'] != 'maintenance':
prod = state['factory_ryazan_bitumen']['current_output'] * random.uniform(0.9, 1.1)
state['factory_ryazan_bitumen']['ready_to_ship_stock'] += prod
if random.random() < 0.05: state['factory_ryazan_bitumen']['status'] = 'maintenance'
else:
if random.random() < 0.2: state['factory_ryazan_bitumen']['status'] = 'normal'
# ==========================================
# 2. ШТАТНАЯ ЛОГИСТИКА (Автопополнение СКЛАДОВ)
# ==========================================
# Базальт: Учалы -> Склад Дзержинска
if state['warehouse_dzerzhinsk']['current_stock'] < state['warehouse_dzerzhinsk']['max_capacity']:
transfer = min(50, state['factory_uchaly_basalt']['ready_to_ship_stock'])
state['factory_uchaly_basalt']['ready_to_ship_stock'] -= transfer
state['warehouse_dzerzhinsk']['current_stock'] += transfer
# Стеклохолст: Воскресенск -> Склад Рязани
if state['warehouse_ryazan']['current_stock'] < state['warehouse_ryazan']['max_capacity']:
transfer = min(30, state['factory_voskresensk_fiberglass']['ready_to_ship_stock'])
state['factory_voskresensk_fiberglass']['ready_to_ship_stock'] -= transfer
state['warehouse_ryazan']['current_stock'] += transfer
# ==========================================
# 3. ПРОИЗВОДСТВО И ПОТРЕБЛЕНИЕ (Завод Черепицы)
# ==========================================
shingles_factory = state['factory_ryazan_shingles']
bom = shingles_factory['bom_per_ton']
# Целевое производство (с рандомом)
target_production = shingles_factory['daily_production_target'] * random.uniform(0.8, 1.0)
# Проверяем, хватает ли сырья для производства (поиск "бутылочного горлышка" в реальном времени)
can_produce = target_production
for mat, ratio in bom.items():
needed = target_production * ratio
available = shingles_factory['raw_materials_on_site'][mat]
if available < needed:
can_produce = min(can_produce, available / ratio if ratio > 0 else 0)
# Фактическое производство готовой черепицы
actual_production = max(0, can_produce)
shingles_factory['current_stock_shingles'] += actual_production
# Списание сырья согласно BOM
for mat, ratio in bom.items():
consumed = actual_production * ratio
shingles_factory['raw_materials_on_site'][mat] = max(0, shingles_factory['raw_materials_on_site'][mat] - consumed)
# ==========================================
# 4. ШТАТНАЯ ЛОГИСТИКА (Автопополнение ЗАВОДА)
# ==========================================
# Базальт: Склад Дзержинска -> Завод Рязань
needed_basalt = (shingles_factory['daily_production_target'] * bom['basalt_granulate'] * 2) - shingles_factory['raw_materials_on_site']['basalt_granulate']
if needed_basalt > 0 and state['warehouse_dzerzhinsk']['current_stock'] > 20:
transfer = min(30, needed_basalt, state['warehouse_dzerzhinsk']['current_stock'])
state['warehouse_dzerzhinsk']['current_stock'] -= transfer
shingles_factory['raw_materials_on_site']['basalt_granulate'] += transfer
# Стеклохолст: Склад Рязани -> Завод Рязань
needed_fiberglass = (shingles_factory['daily_production_target'] * bom['fiberglass'] * 2) - shingles_factory['raw_materials_on_site']['fiberglass']
if needed_fiberglass > 0 and state['warehouse_ryazan']['current_stock'] > 10:
transfer = min(15, needed_fiberglass, state['warehouse_ryazan']['current_stock'])
state['warehouse_ryazan']['current_stock'] -= transfer
shingles_factory['raw_materials_on_site']['fiberglass'] += transfer
# Битум: Завод Битума -> Завод Черепицы (напрямую, минуя склад)
needed_bitumen = (shingles_factory['daily_production_target'] * bom['bitumen'] * 2) - shingles_factory['raw_materials_on_site']['bitumen']
if needed_bitumen > 0 and state['factory_ryazan_bitumen']['ready_to_ship_stock'] > 10:
transfer = min(20, needed_bitumen, state['factory_ryazan_bitumen']['ready_to_ship_stock'])
state['factory_ryazan_bitumen']['ready_to_ship_stock'] -= transfer
shingles_factory['raw_materials_on_site']['bitumen'] += transfer
# ==========================================
# 5. ГЕНЕРАТОР ХАОСА (Инциденты на реальных маршрутах)
# ==========================================
incidents = []
if random.random() < 0.20: # 20% шанс инцидента за тик
incident_type = random.choice(['traffic_jam', 'truck_breakdown', 'weather_delay', 'road_closure'])
hours = random.randint(2, 10)
location = random.choice([
'Учалы-Дзержинск', 'Дзержинск-Рязань',
'Воскресенск-Рязань', 'Рязань_Склад-Рязань_Завод',
'Рязань_Битум-Рязань_Завод'
])
incidents.append({"type": incident_type, "hours_delay": hours, "location": location})
# ==========================================
# 6. ДИНАМИКА АВТОПАРКА
# ==========================================
for truck in state['trucks']:
if truck['status'] == 'on_route':
truck['eta_hours'] = max(0, truck['eta_hours'] - 1)
if truck['eta_hours'] == 0:
truck['status'] = 'available'
elif truck['status'] == 'available' and random.random() < 0.03:
# 3% шанс поломки свободной машины
truck['status'] = 'maintenance'
truck['eta_hours'] = random.randint(2, 6)
elif truck['status'] == 'maintenance':
truck['eta_hours'] = max(0, truck['eta_hours'] - 1)
if truck['eta_hours'] == 0:
truck['status'] = 'available'
state['incidents'] = incidents
return {"updated_state": state}
updated_state
Type = Object
state → updated_state
import json
def main(state: dict) -> dict:
shingles_factory = state['factory_ryazan_shingles']
bom = shingles_factory['bom_per_ton']
daily_target = shingles_factory['daily_production_target']
raw_stock = shingles_factory['raw_materials_on_site']
# 1. Суточная потребность
daily_needs = {
"bitumen": daily_target * bom["bitumen"],
"basalt_granulate": daily_target * bom["basalt_granulate"],
"fiberglass": daily_target * bom["fiberglass"]
}
# 2. Горизонт обеспеченности (на сколько дней хватит)
days_left = {}
for mat, need in daily_needs.items():
days_left[mat] = round(raw_stock[mat] / need, 2) if need > 0 else 999
# 3. Поиск самого "узкого" места
critical_material = min(days_left, key=days_left.get)
min_days = days_left[critical_material]
# 4. ВВОДИМ ПОРОГ КРИТИЧНОСТИ
# Если самому дефицитному материалу хватает на 3+ дня, то глобального кризиса НЕТ.
is_critical = min_days < 3.0
# 5. Считаем объем дозаказа ТОЛЬКО если есть кризис
if is_critical:
needed_volume = (daily_needs[critical_material] * 3) - raw_stock[critical_material]
needed_volume = max(0, needed_volume)
else:
needed_volume = 0 # В штатном режиме экстренный подвоз не нужен
# 6. Логистические опции (формируем только для критичного материала)
logistics_options = {}
if critical_material == "bitumen":
trucks = [t for t in state['trucks'] if t['base'] == 'Рязань' and t['material'] in ['Битум', 'Любой (Локальный)'] and t['status'] == 'available']
logistics_options["direct_bitumen"] = {
"route": "Завод Битума -> Завод Черепицы (Прямая)",
"time_hours": 1,
"available_trucks": [t['id'] for t in trucks],
"source_stock": state['factory_ryazan_bitumen']['ready_to_ship_stock']
}
elif critical_material == "basalt_granulate":
trucks = [t for t in state['trucks'] if t['base'] == 'Дзержинск' and t['status'] == 'available']
logistics_options["warehouse_basalt"] = {
"route": "Склад Дзержинск -> Завод Черепицы",
"time_hours": 7,
"available_trucks": [t['id'] for t in trucks],
"source_stock": state['warehouse_dzerzhinsk']['current_stock']
}
elif critical_material == "fiberglass":
trucks = [t for t in state['trucks'] if t['base'] == 'Рязань' and t['material'] in ['Стеклохолст', 'Любой (Локальный)'] and t['status'] == 'available']
logistics_options["warehouse_fiberglass"] = {
"route": "Склад Рязань -> Завод Черепицы (Локальная)",
"time_hours": 1,
"available_trucks": [t['id'] for t in trucks],
"source_stock": state['warehouse_ryazan']['current_stock']
}
# 7. Контекст для LLM
context = {
"daily_target_shingles_tons": daily_target,
"bom_analysis": {
"bitumen_days_left": days_left['bitumen'],
"basalt_days_left": days_left['basalt_granulate'],
"fiberglass_days_left": days_left['fiberglass']
},
"bottleneck_material": critical_material,
"bottleneck_days_left": min_days,
"is_critical": is_critical,
"needed_volume_tons": round(needed_volume, 2),
"logistics_options": logistics_options,
"incidents": state.get('incidents', [])
}
return {"planner_context": json.dumps(context, ensure_ascii=False)}
planner_context
Type = String
Модель = gpt-4o-mini
Temperature = 0,4
Max Tokens = 512
Response Format = Text
Ты — старший аналитик цепочки поставок производства гибкой черепицы.
Тебе передан цифровой контекст, где Python-алгоритм УЖЕ рассчитал спецификацию (BOM) и нашел "Бутылочное горлышко" (материал, который закончится раньше всего).
Тебе переданы:
1. Цифровой контекст (запасы, маршруты, машины).
2. Экстренные новости (текст от оператора).
Твоя задача:
1. Проверь поле `bottleneck_material`. Это твой главный фокус. Не трать время на материалы, которых хватает на 3+ дней.
2. Поле `is_critical` — это главный переключатель. Если `is_critical == false`, это значит, что запасы в норме (хватает на 3+ дня). В этом случае `alert_level` ДОЛЖЕН БЫТЬ "green" или "yellow". В `primary_action` напиши: "Штатная работа, экстренные доставки не требуются". Если `is_critical == true`, только тогда формируй план экстренной доставки.
3. Объем доставки: Ты ОБЯЗАН использовать значение из поля `needed_volume_tons` для объема доставки. НЕ ВЫДУМЫВАЙ объем
4. Если `needed_volume_tons == 0`, не предлагай никаких рейсов.
5. Посмотри на `logistics_options`. Там указан конкретный маршрут, время и список свободных машин ID.
6. Если свободных машин нет или на источнике (source_stock) нет нужного объема (`needed_volume_tons`), это КРИТИЧЕСКИЙ СБОЙ ЛОГИСТИКИ.
7. Сформулируй четкий план: какие именно машины (по ID из списка) нужно отправить, сколько тонн они повезут.
8. Если есть внешние новости (форс-мажоры), адаптируй план под них.
Выведи свои рассуждения обычным текстом шаг за шагом. Не пытайся сразу форматировать их в JSON.
Цифровой контекст: /planner_context
Экстренные новости: /breaking_news
Установить тумблер Structured у параметра OUTPUT Variables
Модель = gpt-4o-mini
Temperature = 0,1
Max Tokens = 512
Response Format = JSON object
Ты — строгий форматировщик данных. Тебе передали готовое решение и обоснование от старшего аналитика.
Твоя ЕДИНСТВЕННАЯ задача — перевести его решение в строгий JSON формат.
Используй ТОЛЬКО эти ключи:
{
"alert_level": "green" | "yellow" | "red",
"primary_action": "строка",
"estimated_time_hours": число,
"estimated_cost_rub": число,
"business_justification": "строка"
}
Никакого текста до или после JSON.
Готовое решение: /text
Установить тумблер Structured у параметра OUTPUT Variables
state → Updater-agent.updated_state
recommendation → LLM-Formatter.text
import json
def main(state: dict, recommendation: str) -> dict:
# 1. Безопасно парсим ответ LLM
rec_text = recommendation.replace('```json', '').replace('```', '').strip()
try:
rec = json.loads(rec_text)
except Exception:
rec = {}
alert_level = str(rec.get('alert_level', 'red')).lower()
primary_action = rec.get('primary_action', 'Ошибка: LLM не вернула план действий')
justification = rec.get('business_justification', 'Обоснование не предоставлено')
est_time = rec.get('estimated_time_hours', 'N/A')
est_cost = rec.get('estimated_cost_rub', 'N/A')
if alert_level not in ['green', 'yellow', 'red']:
alert_level = 'red'
alert_emoji = "🟢" if alert_level == 'green' else ("🟡" if alert_level == 'yellow' else "🔴")
# 2. Достаем данные из новой архитектуры
shingles = state['factory_ryazan_shingles']
raw_mat = shingles['raw_materials_on_site']
wh_ryazan = state['warehouse_ryazan']['current_stock']
wh_dzerzhinsk = state['warehouse_dzerzhinsk']['current_stock']
bitumen_ready = state['factory_ryazan_bitumen']['ready_to_ship_stock']
basalt_ready = state['factory_uchaly_basalt']['ready_to_ship_stock']
fiberglass_ready = state['factory_voskresensk_fiberglass']['ready_to_ship_stock']
# 3. Формируем красивый Markdown-отчет
markdown_report = f"""
### {alert_emoji} Статус производства: {alert_level.upper()}
#### 🏭 Завод гибкой черепицы (Рязань)
| Сырье / Продукт | Запас на заводе (т) | Готово к отгрузке (т) |
| :--- | :--- | :--- |
| **Битум** | {round(raw_mat['bitumen'], 1)} | {round(bitumen_ready, 1)} |
| **Базальтовая крошка** | {round(raw_mat['basalt_granulate'], 1)} | {round(basalt_ready, 1)} |
| **Стеклохолст** | {round(raw_mat['fiberglass'], 1)} | {round(fiberglass_ready, 1)} |
| **Готовая черепица** | {round(shingles['current_stock_shingles'], 1)} | - |
#### 📦 Склады и Промежуточные точки
| Локация | Материал | Текущий запас (т) |
| :--- | :--- | :--- |
| Склад (Рязань) | Стеклохолст | {round(wh_ryazan, 1)} |
| Склад (Дзержинск) | Базальтовая крошка | {round(wh_dzerzhinsk, 1)} |
🚨 **Активных инцидентов на маршрутах:** {len(state.get('incidents', []))}
---
#### 🚚 Рекомендация Планировщика
> **{primary_action}**
💡 *{justification}*
⏱ **Время:** {est_time} ч. | 💰 **Стоимость:** {est_cost} руб.
"""
# ВАЖНО: Имя ключа 'html_content' должно совпадать с Output Variable в настройках узла Dify!
return {"html_content": markdown_report}
html_content
Type = string
dashboard_md → Report_generator.md_content
{
"factory_uchaly_basalt": {
"id": "F-UB-01",
"material": "Базальтовая крошка",
"city": "Учалы",
"daily_output": 150,
"current_output": 145,
"status": "normal",
"ready_to_ship_stock": 400
},
"factory_ryazan_bitumen": {
"id": "F-RB-01",
"material": "Битум",
"city": "Рязань",
"daily_output": 200,
"current_output": 190,
"status": "normal",
"ready_to_ship_stock": 50
},
"factory_voskresensk_fiberglass": {
"id": "F-VF-01",
"material": "Стеклохолст",
"city": "Воскресенск",
"daily_output": 80,
"current_output": 75,
"status": "normal",
"ready_to_ship_stock": 120
},
"factory_ryazan_shingles": {
"id": "F-RS-01",
"product": "Гибкая черепица",
"city": "Рязань",
"daily_production_target": 20,
"current_stock_shingles": 15,
"safety_stock_shingles": 10,
"bom_per_ton": {
"bitumen": 0.50,
"basalt_granulate": 0.35,
"fiberglass": 0.15
},
"raw_materials_on_site": {
"bitumen": 8.0,
"basalt_granulate": 5.0,
"fiberglass": 2.5
}
},
"warehouse_ryazan": {
"id": "W-RZ-01",
"material_stored": "Стеклохолст",
"city": "Рязань",
"current_stock": 25.0,
"max_capacity": 100
},
"warehouse_dzerzhinsk": {
"id": "W-DZ-01",
"material_stored": "Базальтовая крошка",
"city": "Дзержинск",
"current_stock": 150.0,
"max_capacity": 1000
},
"trucks": [
{"id": "T-UB-01", "base": "Учалы", "capacity": 20, "status": "available", "material": "Базальтовая крошка"},
{"id": "T-UB-02", "base": "Учалы", "capacity": 20, "status": "available", "material": "Базальтовая крошка"},
{"id": "T-DZ-01", "base": "Дзержинск", "capacity": 15, "status": "available", "material": "Базальтовая крошка"},
{"id": "T-DZ-02", "base": "Дзержинск", "capacity": 15, "status": "on_route", "eta_hours": 7, "material": "Базальтовая крошка"},
{"id": "T-VF-01", "base": "Воскресенск", "capacity": 10, "status": "available", "material": "Стеклохолст"},
{"id": "T-VF-02", "base": "Воскресенск", "capacity": 10, "status": "available", "material": "Стеклохолст"},
{"id": "T-RZ-01", "base": "Рязань", "capacity": 5, "status": "available", "material": "Стеклохолст"},
{"id": "T-RB-01", "base": "Рязань", "capacity": 10, "status": "available", "material": "Битум"},
{"id": "T-RB-02", "base": "Рязань", "capacity": 10, "status": "maintenance", "eta_hours": 4, "material": "Битум"},
{"id": "T-LOC-01", "base": "Рязань", "capacity": 5, "status": "available", "material": "Любой (Локальный)"},
{"id": "T-LOC-02", "base": "Рязань", "capacity": 5, "status": "available", "material": "Любой (Локальный)"}
],
"routes": [
{"from": "Учалы", "to": "Дзержинск", "distance": 1100, "hours": 18, "cost_km": 40},
{"from": "Дзержинск", "to": "Рязань", "distance": 450, "hours": 7, "cost_km": 45},
{"from": "Воскресенск", "to": "Рязань", "distance": 180, "hours": 3, "cost_km": 45},
{"from": "Рязань_Склад", "to": "Рязань_Завод", "distance": 20, "hours": 1, "cost_km": 60},
{"from": "Рязань_Битум", "to": "Рязань_Завод", "distance": 15, "hours": 1, "cost_km": 60},
{"from": "Учалы", "to": "Рязань", "distance": 1300, "hours": 20, "cost_km": 40}
],
"incidents": []
}