From 253268aaed55767467b404f41a667ff2fafc204d Mon Sep 17 00:00:00 2001 From: Mohammad Mowas Date: Tue, 28 Oct 2025 05:27:56 +0300 Subject: [PATCH] Add Cognitive Manufacturing Environment A comprehensive manufacturing control environment with 30 tools for AI agents. Features: - Multi-machine production line simulation - Database persistence (PostgreSQL/SQLite support) - ML-powered analytics (predictive maintenance, anomaly detection, quality prediction) - Reinforcement learning optimization - Quality and inventory management - Energy monitoring and optimization - Scenario simulation and schedule optimization Architecture: - Physics-based simulator with temperature, vibration, and wear dynamics - Multi-objective reward system - 30 specialized tools across manufacturing operations - Complete OpenEnv compliance Author: Mohammad Mowas Dependencies: sqlalchemy, sentence-transformers, pandas, scikit-learn --- src/envs/cognitive_manufacturing/.gitignore | 50 ++ src/envs/cognitive_manufacturing/README.md | 224 +++++++ src/envs/cognitive_manufacturing/__init__.py | 34 ++ src/envs/cognitive_manufacturing/client.py | 94 +++ src/envs/cognitive_manufacturing/models.py | 174 ++++++ .../cognitive_manufacturing/server/Dockerfile | 38 ++ .../server/__init__.py | 14 + .../cognitive_manufacturing/server/app.py | 32 + .../server/csv_service.py | 178 ++++++ .../server/database.py | 557 ++++++++++++++++++ .../server/embeddings.py | 68 +++ .../server/environment.py | 496 ++++++++++++++++ .../server/ml_models.py | 517 ++++++++++++++++ .../server/production_line.py | 413 +++++++++++++ .../cognitive_manufacturing/server/rewards.py | 216 +++++++ .../server/simulator.py | 309 ++++++++++ .../cognitive_manufacturing/tools/__init__.py | 78 +++ .../tools/add_knowledge.py | 100 ++++ .../tools/adjust_speed.py | 88 +++ .../cognitive_manufacturing/tools/base.py | 118 ++++ .../tools/check_health.py | 83 +++ .../tools/check_inventory.py | 129 ++++ .../tools/detect_anomaly.py | 123 ++++ .../tools/execute_sql.py | 97 +++ .../tools/export_to_csv.py | 166 ++++++ .../tools/forecast_demand.py | 159 +++++ .../tools/get_line_status.py | 99 ++++ .../tools/import_from_csv.py | 148 +++++ .../tools/inspect_product.py | 181 ++++++ .../tools/monitor_energy_usage.py | 164 ++++++ .../tools/optimize_line_speed.py | 148 +++++ .../tools/optimize_production_schedule.py | 219 +++++++ .../tools/optimize_with_rl.py | 146 +++++ .../tools/order_materials.py | 161 +++++ .../tools/predict_maintenance.py | 136 +++++ .../tools/predict_quality.py | 142 +++++ .../tools/query_production_history.py | 153 +++++ .../tools/read_sensors.py | 68 +++ .../tools/record_defect.py | 136 +++++ .../tools/save_production_data.py | 117 ++++ .../tools/schedule_maintenance.py | 102 ++++ .../tools/search_knowledge.py | 115 ++++ .../tools/send_alert.py | 82 +++ .../tools/set_power_mode.py | 162 +++++ .../tools/simulate_scenario.py | 200 +++++++ .../tools/transfer_material.py | 127 ++++ .../tools/update_qc_thresholds.py | 147 +++++ .../tools/update_stock_levels.py | 156 +++++ 48 files changed, 7664 insertions(+) create mode 100644 src/envs/cognitive_manufacturing/.gitignore create mode 100644 src/envs/cognitive_manufacturing/README.md create mode 100644 src/envs/cognitive_manufacturing/__init__.py create mode 100644 src/envs/cognitive_manufacturing/client.py create mode 100644 src/envs/cognitive_manufacturing/models.py create mode 100644 src/envs/cognitive_manufacturing/server/Dockerfile create mode 100644 src/envs/cognitive_manufacturing/server/__init__.py create mode 100644 src/envs/cognitive_manufacturing/server/app.py create mode 100644 src/envs/cognitive_manufacturing/server/csv_service.py create mode 100644 src/envs/cognitive_manufacturing/server/database.py create mode 100644 src/envs/cognitive_manufacturing/server/embeddings.py create mode 100644 src/envs/cognitive_manufacturing/server/environment.py create mode 100644 src/envs/cognitive_manufacturing/server/ml_models.py create mode 100644 src/envs/cognitive_manufacturing/server/production_line.py create mode 100644 src/envs/cognitive_manufacturing/server/rewards.py create mode 100644 src/envs/cognitive_manufacturing/server/simulator.py create mode 100644 src/envs/cognitive_manufacturing/tools/__init__.py create mode 100644 src/envs/cognitive_manufacturing/tools/add_knowledge.py create mode 100644 src/envs/cognitive_manufacturing/tools/adjust_speed.py create mode 100644 src/envs/cognitive_manufacturing/tools/base.py create mode 100644 src/envs/cognitive_manufacturing/tools/check_health.py create mode 100644 src/envs/cognitive_manufacturing/tools/check_inventory.py create mode 100644 src/envs/cognitive_manufacturing/tools/detect_anomaly.py create mode 100644 src/envs/cognitive_manufacturing/tools/execute_sql.py create mode 100644 src/envs/cognitive_manufacturing/tools/export_to_csv.py create mode 100644 src/envs/cognitive_manufacturing/tools/forecast_demand.py create mode 100644 src/envs/cognitive_manufacturing/tools/get_line_status.py create mode 100644 src/envs/cognitive_manufacturing/tools/import_from_csv.py create mode 100644 src/envs/cognitive_manufacturing/tools/inspect_product.py create mode 100644 src/envs/cognitive_manufacturing/tools/monitor_energy_usage.py create mode 100644 src/envs/cognitive_manufacturing/tools/optimize_line_speed.py create mode 100644 src/envs/cognitive_manufacturing/tools/optimize_production_schedule.py create mode 100644 src/envs/cognitive_manufacturing/tools/optimize_with_rl.py create mode 100644 src/envs/cognitive_manufacturing/tools/order_materials.py create mode 100644 src/envs/cognitive_manufacturing/tools/predict_maintenance.py create mode 100644 src/envs/cognitive_manufacturing/tools/predict_quality.py create mode 100644 src/envs/cognitive_manufacturing/tools/query_production_history.py create mode 100644 src/envs/cognitive_manufacturing/tools/read_sensors.py create mode 100644 src/envs/cognitive_manufacturing/tools/record_defect.py create mode 100644 src/envs/cognitive_manufacturing/tools/save_production_data.py create mode 100644 src/envs/cognitive_manufacturing/tools/schedule_maintenance.py create mode 100644 src/envs/cognitive_manufacturing/tools/search_knowledge.py create mode 100644 src/envs/cognitive_manufacturing/tools/send_alert.py create mode 100644 src/envs/cognitive_manufacturing/tools/set_power_mode.py create mode 100644 src/envs/cognitive_manufacturing/tools/simulate_scenario.py create mode 100644 src/envs/cognitive_manufacturing/tools/transfer_material.py create mode 100644 src/envs/cognitive_manufacturing/tools/update_qc_thresholds.py create mode 100644 src/envs/cognitive_manufacturing/tools/update_stock_levels.py diff --git a/src/envs/cognitive_manufacturing/.gitignore b/src/envs/cognitive_manufacturing/.gitignore new file mode 100644 index 00000000..6b47aad4 --- /dev/null +++ b/src/envs/cognitive_manufacturing/.gitignore @@ -0,0 +1,50 @@ +# Documentation files (keep in separate docs repo) +*.md +!README.md + +# Python cache +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python + +# Virtual environment +venv/ +env/ +ENV/ + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Database files +*.db +*.sqlite +*.sqlite3 + +# Data directories +data/ +exports/ + +# Logs +*.log +server.log +nohup.out +server.pid + +# OS +.DS_Store +Thumbs.db + +# Testing +.pytest_cache/ +.coverage +htmlcov/ + +# Temporary files +*.tmp +*.bak diff --git a/src/envs/cognitive_manufacturing/README.md b/src/envs/cognitive_manufacturing/README.md new file mode 100644 index 00000000..abe056b2 --- /dev/null +++ b/src/envs/cognitive_manufacturing/README.md @@ -0,0 +1,224 @@ +# Cognitive Manufacturing Environment + +An OpenEnv-compliant environment for AI agents to control and optimize manufacturing operations with 30 specialized tools across quality management, inventory control, energy optimization, and predictive analytics. + +**Author**: Mohammad Mowas | is.mohammad@hotmail.com + +## Overview + +The Cognitive Manufacturing Environment provides a comprehensive simulation of a production facility where AI agents must balance multiple objectives: +- **Safety** (highest priority): Prevent failures, overheating, excessive vibration +- **Throughput**: Maximize production output +- **Quality**: Minimize defects +- **Cost**: Optimize operational expenses +- **Sustainability**: Improve energy efficiency + +## Features + +- **30 specialized tools** for complete manufacturing control +- **Multi-machine production line** simulation (4-stage pipeline) +- **Database persistence** (PostgreSQL/SQLite support) +- **ML-powered analytics** (predictive maintenance, anomaly detection, quality prediction) +- **Reinforcement learning** optimization (Q-learning) +- **Quality management** with defect tracking +- **Inventory control** and material ordering +- **Energy monitoring** and power optimization +- **Scenario simulation** and schedule optimization + +## Quick Start + +### Installation + +```bash +# Install OpenEnv +pip install -e . + +# Install cognitive manufacturing dependencies +pip install sqlalchemy sentence-transformers pandas scikit-learn numpy scipy +``` + +### Basic Usage + +```python +from envs.cognitive_manufacturing import CognitiveManufacturingEnvironment, ManufacturingAction + +# Create environment with all features enabled +env = CognitiveManufacturingEnvironment( + multi_machine=True, # Enable 4-machine production line + enable_database=True, # Enable data persistence + enable_ml=True, # Enable ML analytics +) + +# Reset environment +observation = env.reset() + +# Take an action +action = ManufacturingAction( + tool_name="ReadSensors", + parameters={"machine_id": "M1"} +) +observation, reward, done, info = env.step(action) +``` + +### Server Mode + +```bash +# Start HTTP server +python -m uvicorn envs.cognitive_manufacturing.server.app:app --host 127.0.0.1 --port 8001 + +# Server will be available at http://127.0.0.1:8001 +``` + +### Docker Deployment + +```bash +docker build -t cognitive-manufacturing:latest -f src/envs/cognitive_manufacturing/server/Dockerfile . +docker run -p 8001:8001 cognitive-manufacturing:latest +``` + +## Available Tools (30 total) + +### Basic Control (5 tools) +1. **ReadSensors** - Get real-time sensor data +2. **CheckHealth** - Health diagnostics +3. **AdjustSpeed** - Control machine speed (0-100%) +4. **ScheduleMaintenance** - Schedule/perform maintenance +5. **SendAlert** - Send notifications + +### Production Line (3 tools) +6. **GetLineStatus** - Production line status +7. **TransferMaterial** - Material transfer between machines +8. **OptimizeLineSpeed** - Multi-machine speed optimization + +### Data Management (7 tools) +9. **SaveProductionData** - Save runs to database +10. **QueryProductionHistory** - Query historical data +11. **ExecuteSQL** - Custom SQL queries +12. **ExportToCSV** - Export data to CSV +13. **ImportFromCSV** - Import data from CSV +14. **SearchKnowledge** - Semantic search in knowledge base +15. **AddKnowledge** - Add documents to knowledge base + +### ML Analytics (5 tools) +16. **PredictMaintenance** - Failure prediction with Random Forest +17. **DetectAnomaly** - Anomaly detection with Isolation Forest +18. **PredictQuality** - Quality prediction with Linear Regression +19. **OptimizeWithRL** - Reinforcement learning optimization (Q-learning) +20. **ForecastDemand** - Time series demand forecasting + +### Advanced Management (10 tools) +21. **InspectProduct** - Detailed quality inspection (visual/dimensional/functional) +22. **RecordDefect** - Log and track defects with severity levels +23. **UpdateQCThresholds** - Adjust quality control parameters +24. **CheckInventory** - Inventory levels with reorder recommendations +25. **OrderMaterials** - Material ordering with priority +26. **UpdateStockLevels** - Update inventory (add/remove/set) +27. **MonitorEnergyUsage** - Track power consumption and costs +28. **SetPowerMode** - Adjust power modes (eco/normal/high_performance) +29. **SimulateScenario** - What-if scenario simulations +30. **OptimizeProductionSchedule** - Production schedule optimization + +## Environment Modes + +### Single Machine Mode +```python +env = CognitiveManufacturingEnvironment() +``` +- 1 machine (M1) +- Basic production simulation + +### Production Line Mode +```python +env = CognitiveManufacturingEnvironment(multi_machine=True) +``` +- 4 machines (M1 → M2 → M3 → M4) +- Material flow through buffers +- Bottleneck detection and optimization + +### Full Feature Mode +```python +env = CognitiveManufacturingEnvironment( + multi_machine=True, + enable_database=True, + enable_ml=True +) +``` +- All 30 tools available +- Database persistence +- ML-powered analytics + +## Machine Simulation + +The physics-based simulator models realistic machine behavior: +- **Temperature dynamics**: Heat generation from operation, cooling over time +- **Vibration**: Increases with speed and wear +- **Wear accumulation**: Gradual degradation during operation +- **Failure probability**: Based on temperature, vibration, and wear levels +- **Production output**: Speed-dependent with quality penalties +- **Energy consumption**: Power usage based on speed and operating mode + +## Reward System + +Multi-objective reward calculation: +- **Safety**: Penalty for dangerous conditions (overheating, high vibration) +- **Throughput**: Reward for production output +- **Quality**: Penalty for defects +- **Cost**: Penalty for operational expenses +- **Sustainability**: Reward for energy efficiency + +## Database Schema + +When `enable_database=True`, the environment persists: +- Production runs with metadata +- Sensor readings (temperature, vibration, speed, health) +- Machine events (speed changes, maintenance, alerts) +- Production units with quality scores +- Knowledge base with vector embeddings + +Supports PostgreSQL and SQLite. + +## ML Models + +When `enable_ml=True`, the environment provides: +- **Random Forest** for predictive maintenance +- **Isolation Forest** for anomaly detection +- **Linear Regression** for quality prediction +- **Q-learning** for reinforcement learning optimization +- **Exponential Smoothing** for demand forecasting + +Models train automatically from historical data when sufficient samples are available. + +## Architecture + +``` +CognitiveManufacturingEnvironment +├── Simulator (physics-based machine dynamics) +├── Production Line (4-machine pipeline with buffers) +├── Database Manager (PostgreSQL/SQLite persistence) +├── ML Service (5 ML models) +├── Reward Calculator (multi-objective rewards) +└── Tools (30 specialized tools) +``` + +## Dependencies + +- **Core**: OpenEnv framework +- **Database**: SQLAlchemy +- **ML**: scikit-learn, numpy, scipy +- **NLP**: sentence-transformers (for semantic search) +- **Data**: pandas (for CSV operations) + +## Examples + +See OpenEnv `examples/` directory for usage examples. + +## License + +MIT License - See LICENSE file for details. + +## Author + +**Mohammad Mowas** +Email: is.mohammad@hotmail.com + +Developed as part of the OpenEnv framework for AI agent environments. diff --git a/src/envs/cognitive_manufacturing/__init__.py b/src/envs/cognitive_manufacturing/__init__.py new file mode 100644 index 00000000..0de125e2 --- /dev/null +++ b/src/envs/cognitive_manufacturing/__init__.py @@ -0,0 +1,34 @@ +"""Cognitive Manufacturing Environment - OpenEnv compliant manufacturing simulation. + +This package provides an environment for AI agents to manage and optimize +a simulated manufacturing system with real-time sensor data, health monitoring, +and multi-objective optimization. + +Phase 0 MVP Features: +- Single simulated production machine +- 5 tools: ReadSensors, CheckHealth, AdjustSpeed, ScheduleMaintenance, SendAlert +- Multi-objective rewards: Safety, Throughput, Quality, Cost, Sustainability +- Realistic physics simulation with temperature, vibration, and wear dynamics +""" + +from .models import ( + ManufacturingAction, + ManufacturingObservation, + ManufacturingState, + MachineStatus, + Alert, +) +from .client import CognitiveManufacturingEnv +from .server import CognitiveManufacturingEnvironment + +__version__ = "0.1.0" + +__all__ = [ + "ManufacturingAction", + "ManufacturingObservation", + "ManufacturingState", + "MachineStatus", + "Alert", + "CognitiveManufacturingEnv", + "CognitiveManufacturingEnvironment", +] diff --git a/src/envs/cognitive_manufacturing/client.py b/src/envs/cognitive_manufacturing/client.py new file mode 100644 index 00000000..f1df1fa1 --- /dev/null +++ b/src/envs/cognitive_manufacturing/client.py @@ -0,0 +1,94 @@ +"""HTTP client for cognitive manufacturing environment. + +This module provides a convenient wrapper for interacting with the +manufacturing environment server via HTTP. +""" + +from __future__ import annotations +from typing import Any +from dataclasses import asdict +from core.http_env_client import HTTPEnvClient +from core.client_types import StepResult +from .models import ManufacturingAction, ManufacturingObservation + + +class CognitiveManufacturingEnv(HTTPEnvClient[ManufacturingAction, ManufacturingObservation]): + """HTTP client for cognitive manufacturing environment. + + This client connects to a running manufacturing environment server + and provides a simple interface for agents to interact with it. + + Usage: + >>> env = CognitiveManufacturingEnv(base_url="http://localhost:8000") + >>> result = env.reset() + >>> obs = result.observation + >>> action = ManufacturingAction( + ... tool_name="ReadSensors", + ... parameters={"machine_id": "M1", "sensors": "all"} + ... ) + >>> result = env.step(action) + """ + + def __init__(self, base_url: str = "http://localhost:8000"): + """Initialize the client. + + Args: + base_url: Base URL of the environment server + """ + super().__init__(base_url=base_url) + + def _step_payload(self, action: ManufacturingAction) -> dict: + """Convert ManufacturingAction to JSON payload.""" + return asdict(action) + + def _parse_result(self, payload: dict) -> StepResult[ManufacturingObservation]: + """Parse server response into StepResult.""" + # The server returns: {observation, reward, done, info} + obs_data = payload.get("observation", {}) + + # Reconstruct ManufacturingObservation + # Note: This is simplified - in production you'd properly reconstruct the dataclass + observation = ManufacturingObservation( + tool_result=obs_data.get("tool_result", {}), + machine_status=obs_data.get("machine_status"), # Dict format is fine + alerts=obs_data.get("alerts", []), + simulation_time=obs_data.get("simulation_time", 0.0), + ) + + return StepResult( + observation=observation, + reward=payload.get("reward"), + done=payload.get("done", False), + info=payload.get("info", {}), + ) + + def _parse_state(self, payload: dict) -> Any: + """Parse state endpoint response.""" + return payload + + def get_available_tools(self) -> list[str]: + """Get list of available tools from the environment. + + Returns: + List of tool names + """ + state = self.state() + return state.get("available_tools", []) + + def get_machine_status(self) -> dict[str, Any]: + """Get current machine status. + + Returns: + Dictionary with machine status information + """ + state = self.state() + return state.get("machine", {}) + + def get_alerts(self) -> list[dict[str, Any]]: + """Get recent alerts. + + Returns: + List of recent alert dictionaries + """ + state = self.state() + return state.get("alerts", []) diff --git a/src/envs/cognitive_manufacturing/models.py b/src/envs/cognitive_manufacturing/models.py new file mode 100644 index 00000000..2de6baec --- /dev/null +++ b/src/envs/cognitive_manufacturing/models.py @@ -0,0 +1,174 @@ +""" +Data models for Cognitive Manufacturing Environment. + +Defines Action, Observation, and State types for the manufacturing environment. +""" + +from dataclasses import dataclass, field +from typing import Any, Literal + +from core.env_server.types import Action, Observation, State + + +# ============================================================================ +# Actions +# ============================================================================ + + +@dataclass(kw_only=True) +class ManufacturingAction(Action): + """ + Action for cognitive manufacturing environment. + + Uses tool-calling paradigm where agent specifies which tool to use + and provides parameters for that tool. + + Attributes: + tool_name: Name of the tool to execute (e.g., 'ReadSensors', 'AdjustSpeed') + parameters: Tool-specific parameters as a dictionary + reasoning: Optional reasoning for this action (for logging/debugging) + """ + + tool_name: str + parameters: dict[str, Any] = field(default_factory=dict) + reasoning: str | None = None + + +# ============================================================================ +# Observations +# ============================================================================ + + +@dataclass +class MachineStatus: + """Status of a single machine.""" + + machine_id: str + status: Literal["running", "idle", "maintenance", "failed"] + temperature: float # Celsius + vibration: float # arbitrary units (0-1 scale) + speed: float # percentage of max speed (0-100) + health_score: float # 0-100, lower is worse + wear_level: float # 0-1 scale, cumulative wear + production_output: float # units per hour + + +@dataclass +class Alert: + """Alert or warning from the system.""" + + alert_id: str + severity: Literal["info", "warning", "critical"] + machine_id: str | None + message: str + timestamp: float + category: str = "other" # temperature, vibration, wear, health, production, safety, other + + +@dataclass(kw_only=True) +class ManufacturingObservation(Observation): + """ + Observation from cognitive manufacturing environment. + + Inherits from Observation: + - done: bool (episode terminated?) + - reward: float (immediate reward) + - metadata: dict (additional info) + + Additional attributes: + tool_result: Result from the executed tool + machine_status: Current status of the machine + alerts: List of active alerts/warnings + simulation_time: Current simulation time in hours + """ + + tool_result: dict[str, Any] = field(default_factory=dict) + machine_status: MachineStatus | None = None + alerts: list[Alert] = field(default_factory=list) + simulation_time: float = 0.0 + + +# ============================================================================ +# State +# ============================================================================ + + +@dataclass +class ManufacturingState(State): + """ + Extended state for manufacturing environment. + + Inherits from State: + - episode_id: str + - step_count: int + + Additional attributes for manufacturing-specific state tracking. + """ + + simulation_time: float = 0.0 # Hours since episode start + total_units_produced: int = 0 + total_defects: int = 0 + total_downtime: float = 0.0 # Hours + maintenance_scheduled: bool = False + maintenance_time: float | None = None + +# ============================================================================ +# Phase 1: Multi-Machine Production Line Models +# ============================================================================ + + +@dataclass +class ProductUnit: + """A single unit moving through the production line.""" + + unit_id: str + created_at: float # Simulation time when created + stage: str # prep, assembly, finishing, qc + quality: float = 1.0 # 0-1 scale + passed_qc: bool | None = None + defect_type: str | None = None + + +@dataclass +class Buffer: + """Material buffer between machines.""" + + buffer_id: str # e.g., "M1_M2" + capacity: int = 10 + current_level: int = 0 + units: list[ProductUnit] = field(default_factory=list) + + def add_unit(self, unit: ProductUnit) -> bool: + """Add unit to buffer if space available.""" + if self.current_level < self.capacity: + self.units.append(unit) + self.current_level += 1 + return True + return False + + def remove_units(self, count: int) -> list[ProductUnit]: + """Remove and return units from buffer.""" + count = min(count, self.current_level) + removed = self.units[:count] + self.units = self.units[count:] + self.current_level -= count + return removed + + def peek(self, count: int = 1) -> list[ProductUnit]: + """View units without removing.""" + return self.units[:count] + + +@dataclass +class LineMetrics: + """Production line performance metrics.""" + + total_produced: int = 0 + total_defects: int = 0 + total_scrapped: int = 0 + throughput_rate: float = 0.0 # units/hour + qc_pass_rate: float = 1.0 # 0-1 + line_efficiency: float = 1.0 # 0-1 + bottleneck_machine: str | None = None + total_energy: float = 0.0 # kWh + energy_per_unit: float = 0.0 # kWh/unit diff --git a/src/envs/cognitive_manufacturing/server/Dockerfile b/src/envs/cognitive_manufacturing/server/Dockerfile new file mode 100644 index 00000000..306f25ff --- /dev/null +++ b/src/envs/cognitive_manufacturing/server/Dockerfile @@ -0,0 +1,38 @@ +# Dockerfile for Cognitive Manufacturing Environment +# This creates a containerized version of the environment server + +FROM python:3.11-slim + +# Set working directory +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements +COPY requirements.txt . + +# Install Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy OpenEnv (assuming it's in parent directory) +COPY OpenEnv/ /app/OpenEnv/ +RUN pip install --no-cache-dir -e /app/OpenEnv + +# Copy our environment code +COPY src/ /app/src/ + +# Set Python path +ENV PYTHONPATH=/app/src:$PYTHONPATH + +# Expose the server port +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD python -c "import requests; requests.get('http://localhost:8000/health')" + +# Run the server +CMD ["python", "-m", "uvicorn", "envs.cognitive_manufacturing.server.app:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/src/envs/cognitive_manufacturing/server/__init__.py b/src/envs/cognitive_manufacturing/server/__init__.py new file mode 100644 index 00000000..9f4767e9 --- /dev/null +++ b/src/envs/cognitive_manufacturing/server/__init__.py @@ -0,0 +1,14 @@ +"""Server components for cognitive manufacturing environment.""" + +from .environment import CognitiveManufacturingEnvironment +from .simulator import SimulatedMachine +from .rewards import RewardCalculator, RewardWeights +from .app import app + +__all__ = [ + "CognitiveManufacturingEnvironment", + "SimulatedMachine", + "RewardCalculator", + "RewardWeights", + "app", +] diff --git a/src/envs/cognitive_manufacturing/server/app.py b/src/envs/cognitive_manufacturing/server/app.py new file mode 100644 index 00000000..acc51f8b --- /dev/null +++ b/src/envs/cognitive_manufacturing/server/app.py @@ -0,0 +1,32 @@ +"""FastAPI server application for cognitive manufacturing environment. + +This module creates the HTTP server that exposes the environment via REST API, +following the OpenEnv specification. +""" + +from core.env_server import create_fastapi_app +from .environment import CognitiveManufacturingEnvironment +from ..models import ManufacturingAction, ManufacturingObservation + + +# Create environment instance +env = CognitiveManufacturingEnvironment() + +# Create the FastAPI application +app = create_fastapi_app(env, ManufacturingAction, ManufacturingObservation) + + +def main(): + """Entry point for running the server directly.""" + import uvicorn + + uvicorn.run( + "envs.cognitive_manufacturing.server.app:app", + host="0.0.0.0", + port=8000, + reload=True, + ) + + +if __name__ == "__main__": + main() diff --git a/src/envs/cognitive_manufacturing/server/csv_service.py b/src/envs/cognitive_manufacturing/server/csv_service.py new file mode 100644 index 00000000..2c07782c --- /dev/null +++ b/src/envs/cognitive_manufacturing/server/csv_service.py @@ -0,0 +1,178 @@ +"""CSV export and import service for data interchange.""" + +import csv +import os +from pathlib import Path +from typing import Any +from datetime import datetime + +try: + import pandas as pd + PANDAS_AVAILABLE = True +except ImportError: + PANDAS_AVAILABLE = False + + +class CSVService: + """Handles CSV export and import operations.""" + + def __init__(self, export_dir: str = "data/exports"): + """Initialize CSV service. + + Args: + export_dir: Directory for exported CSV files + """ + self.export_dir = Path(export_dir) + self.export_dir.mkdir(parents=True, exist_ok=True) + + def export_to_csv(self, data: list[dict], filename: str) -> str: + """Export data to CSV file. + + Args: + data: List of dictionaries to export + filename: Output filename (with or without .csv extension) + + Returns: + Full path to exported file + """ + if not filename.endswith(".csv"): + filename += ".csv" + + filepath = self.export_dir / filename + + if PANDAS_AVAILABLE: + # Use pandas for better handling of complex data + df = pd.DataFrame(data) + df.to_csv(filepath, index=False) + else: + # Fallback to csv module + if not data: + # Empty data, create empty file + with open(filepath, "w", newline="") as f: + f.write("") + return str(filepath) + + # Get all unique keys from all dictionaries + fieldnames = set() + for row in data: + fieldnames.update(row.keys()) + fieldnames = sorted(fieldnames) + + with open(filepath, "w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(data) + + return str(filepath) + + def import_from_csv(self, filename: str) -> list[dict]: + """Import data from CSV file. + + Args: + filename: Input filename (with or without .csv extension) + + Returns: + List of dictionaries + + Raises: + FileNotFoundError: If file doesn't exist + """ + if not filename.endswith(".csv"): + filename += ".csv" + + # Check multiple locations + possible_paths = [ + Path(filename), # Absolute or relative path + self.export_dir / filename, # In export directory + Path("data") / filename, # In data directory + ] + + filepath = None + for path in possible_paths: + if path.exists(): + filepath = path + break + + if not filepath: + raise FileNotFoundError(f"CSV file not found: {filename}") + + if PANDAS_AVAILABLE: + # Use pandas for better handling + df = pd.read_csv(filepath) + return df.to_dict(orient="records") + else: + # Fallback to csv module + data = [] + with open(filepath, "r", newline="") as f: + reader = csv.DictReader(f) + for row in reader: + data.append(dict(row)) + return data + + def validate_csv(self, filename: str, required_columns: list[str]) -> tuple[bool, str | None]: + """Validate CSV file has required columns. + + Args: + filename: CSV filename + required_columns: List of required column names + + Returns: + Tuple of (is_valid, error_message) + """ + try: + data = self.import_from_csv(filename) + if not data: + return False, "CSV file is empty" + + # Check required columns + first_row = data[0] + missing_columns = [col for col in required_columns if col not in first_row] + + if missing_columns: + return False, f"Missing required columns: {', '.join(missing_columns)}" + + return True, None + + except FileNotFoundError as e: + return False, str(e) + except Exception as e: + return False, f"Error validating CSV: {str(e)}" + + def get_export_path(self, filename: str) -> str: + """Get full path for an export file. + + Args: + filename: Filename + + Returns: + Full path string + """ + if not filename.endswith(".csv"): + filename += ".csv" + return str(self.export_dir / filename) + + def list_exports(self) -> list[str]: + """List all exported CSV files. + + Returns: + List of filenames in export directory + """ + return [f.name for f in self.export_dir.glob("*.csv")] + + def delete_export(self, filename: str) -> bool: + """Delete an exported CSV file. + + Args: + filename: Filename to delete + + Returns: + True if deleted, False if file didn't exist + """ + if not filename.endswith(".csv"): + filename += ".csv" + + filepath = self.export_dir / filename + if filepath.exists(): + filepath.unlink() + return True + return False diff --git a/src/envs/cognitive_manufacturing/server/database.py b/src/envs/cognitive_manufacturing/server/database.py new file mode 100644 index 00000000..c4ae24ec --- /dev/null +++ b/src/envs/cognitive_manufacturing/server/database.py @@ -0,0 +1,557 @@ +"""Database manager for persistent storage of production data.""" + +from __future__ import annotations +import uuid +from datetime import datetime +from typing import Any +from dataclasses import asdict + +try: + from sqlalchemy import ( + create_engine, + Column, + String, + Float, + Integer, + Boolean, + DateTime, + ForeignKey, + Text, + JSON, + ) + from sqlalchemy.ext.declarative import declarative_base + from sqlalchemy.orm import sessionmaker, relationship + from sqlalchemy.dialects.postgresql import UUID, JSONB + SQLALCHEMY_AVAILABLE = True + Base = declarative_base() +except ImportError: + SQLALCHEMY_AVAILABLE = False + # Provide dummy classes for type hints when SQLAlchemy not installed + Base = type('Base', (), {}) # type: ignore + Column = None # type: ignore + String = None # type: ignore + Float = None # type: ignore + Integer = None # type: ignore + Boolean = None # type: ignore + DateTime = None # type: ignore + ForeignKey = None # type: ignore + Text = None # type: ignore + JSON = None # type: ignore + UUID = None # type: ignore + JSONB = None # type: ignore + create_engine = None # type: ignore + sessionmaker = None # type: ignore + relationship = None # type: ignore + + +class ProductionRun(Base): + """Production run record.""" + + __tablename__ = "production_runs" + + run_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + run_name = Column(String(255), nullable=True) + start_time = Column(DateTime, nullable=False) + end_time = Column(DateTime, nullable=True) + mode = Column(String(20), nullable=False) # 'single' or 'multi_machine' + total_steps = Column(Integer, default=0) + simulation_time = Column(Float, default=0.0) + cumulative_reward = Column(Float, default=0.0) + status = Column(String(20), default="in_progress") # 'in_progress', 'completed', 'failed' + run_metadata = Column(JSON, nullable=True) + + # Relationships + sensor_readings = relationship("SensorReading", back_populates="run", cascade="all, delete-orphan") + machine_events = relationship("MachineEvent", back_populates="run", cascade="all, delete-orphan") + production_units = relationship("ProductionUnit", back_populates="run", cascade="all, delete-orphan") + + +class SensorReading(Base): + """Time-series sensor data.""" + + __tablename__ = "sensor_readings" + + reading_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + run_id = Column(UUID(as_uuid=True), ForeignKey("production_runs.run_id"), nullable=False) + machine_id = Column(String(10), nullable=False) + timestamp = Column(DateTime, nullable=False) + simulation_time = Column(Float, nullable=False) + + # Sensor values + temperature = Column(Float) + vibration = Column(Float) + speed = Column(Float) + health_score = Column(Float) + wear_level = Column(Float) + production_output = Column(Float) + status = Column(String(20)) + + # Relationship + run = relationship("ProductionRun", back_populates="sensor_readings") + + +class MachineEvent(Base): + """Machine events (speed changes, maintenance, alerts, etc.).""" + + __tablename__ = "machine_events" + + event_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + run_id = Column(UUID(as_uuid=True), ForeignKey("production_runs.run_id"), nullable=False) + machine_id = Column(String(10), nullable=False) + timestamp = Column(DateTime, nullable=False) + simulation_time = Column(Float, nullable=False) + event_type = Column(String(50), nullable=False) # 'speed_change', 'maintenance', 'alert', etc. + data = Column(JSON, nullable=True) + + # Relationship + run = relationship("ProductionRun", back_populates="machine_events") + + +class ProductionUnit(Base): + """Production unit records (Phase 1).""" + + __tablename__ = "production_units" + + unit_id = Column(String(50), primary_key=True) + run_id = Column(UUID(as_uuid=True), ForeignKey("production_runs.run_id"), nullable=False) + created_at = Column(DateTime, nullable=False) + completed_at = Column(DateTime, nullable=True) + stage = Column(String(20), nullable=False) # 'prep', 'assembly', 'finishing', 'qc' + quality = Column(Float, default=1.0) + quality_score = Column(Float, default=1.0) # Alias for quality + passed_qc = Column(Boolean, nullable=True) + machine_id = Column(String(10), nullable=True) # Machine that produced this unit + unit_metadata = Column(JSON, nullable=True) # Production parameters (speed, temp, etc.) + + # Relationship + run = relationship("ProductionRun", back_populates="production_units") + + +class KnowledgeBase(Base): + """Knowledge base documents with vector embeddings.""" + + __tablename__ = "knowledge_base" + + doc_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + title = Column(String(255), nullable=False) + content = Column(Text, nullable=False) + doc_type = Column(String(50), nullable=False) # 'maintenance', 'troubleshooting', 'safety', etc. + doc_metadata = Column(JSON, nullable=True) + created_at = Column(DateTime, default=datetime.utcnow) + + # Vector embedding stored as JSON array (pgvector would use VECTOR type) + embedding = Column(JSON, nullable=True) + + +class DatabaseManager: + """Manages PostgreSQL connections and operations.""" + + def __init__(self, connection_string: str): + """Initialize database connection. + + Args: + connection_string: PostgreSQL connection string + Example: "postgresql://user:pass@localhost/dbname" + """ + if not SQLALCHEMY_AVAILABLE: + raise ImportError( + "SQLAlchemy is required for database functionality. " + "Install with: pip install sqlalchemy psycopg2-binary" + ) + + self.engine = create_engine(connection_string, echo=False) + self.SessionLocal = sessionmaker(bind=self.engine) + + def create_tables(self): + """Create all database tables if they don't exist.""" + Base.metadata.create_all(self.engine) + + def drop_tables(self): + """Drop all database tables (use with caution!).""" + Base.metadata.drop_all(self.engine) + + # Production Run Operations + + def create_run(self, mode: str, run_name: str | None = None, metadata: dict | None = None) -> str: + """Create a new production run. + + Args: + mode: 'single' or 'multi_machine' + run_name: Optional name for the run + metadata: Optional metadata dict + + Returns: + run_id as string + """ + session = self.SessionLocal() + try: + run = ProductionRun( + run_name=run_name, + start_time=datetime.utcnow(), + mode=mode, + run_metadata=metadata, + ) + session.add(run) + session.commit() + run_id = str(run.run_id) + return run_id + finally: + session.close() + + def complete_run(self, run_id: str, total_steps: int, simulation_time: float, cumulative_reward: float): + """Mark a production run as completed. + + Args: + run_id: Run identifier + total_steps: Total number of steps + simulation_time: Total simulation time (hours) + cumulative_reward: Total cumulative reward + """ + session = self.SessionLocal() + try: + run = session.query(ProductionRun).filter_by(run_id=uuid.UUID(run_id)).first() + if run: + run.end_time = datetime.utcnow() + run.total_steps = total_steps + run.simulation_time = simulation_time + run.cumulative_reward = cumulative_reward + run.status = "completed" + session.commit() + finally: + session.close() + + def save_sensor_reading( + self, + run_id: str, + machine_id: str, + simulation_time: float, + temperature: float, + vibration: float, + speed: float, + health_score: float, + wear_level: float, + production_output: float, + status: str, + ): + """Save a sensor reading to the database. + + Args: + run_id: Production run ID + machine_id: Machine identifier + simulation_time: Simulation time in hours + temperature: Temperature reading + vibration: Vibration reading + speed: Speed reading + health_score: Health score + wear_level: Wear level + production_output: Production output + status: Machine status + """ + session = self.SessionLocal() + try: + reading = SensorReading( + run_id=uuid.UUID(run_id), + machine_id=machine_id, + timestamp=datetime.utcnow(), + simulation_time=simulation_time, + temperature=temperature, + vibration=vibration, + speed=speed, + health_score=health_score, + wear_level=wear_level, + production_output=production_output, + status=status, + ) + session.add(reading) + session.commit() + finally: + session.close() + + def save_machine_event( + self, + run_id: str, + machine_id: str, + simulation_time: float, + event_type: str, + data: dict, + ): + """Save a machine event to the database. + + Args: + run_id: Production run ID + machine_id: Machine identifier + simulation_time: Simulation time in hours + event_type: Type of event + data: Event data dict + """ + session = self.SessionLocal() + try: + event = MachineEvent( + run_id=uuid.UUID(run_id), + machine_id=machine_id, + timestamp=datetime.utcnow(), + simulation_time=simulation_time, + event_type=event_type, + data=data, + ) + session.add(event) + session.commit() + finally: + session.close() + + def query_runs(self, filters: dict | None = None, limit: int = 100) -> list[dict]: + """Query production runs with optional filters. + + Args: + filters: Optional filters dict (start_date, end_date, min_reward, status, mode) + limit: Maximum number of results + + Returns: + List of production run dicts + """ + session = self.SessionLocal() + try: + query = session.query(ProductionRun) + + if filters: + if "start_date" in filters: + query = query.filter(ProductionRun.start_time >= filters["start_date"]) + if "end_date" in filters: + query = query.filter(ProductionRun.start_time <= filters["end_date"]) + if "min_reward" in filters: + query = query.filter(ProductionRun.cumulative_reward >= filters["min_reward"]) + if "status" in filters: + query = query.filter(ProductionRun.status == filters["status"]) + if "mode" in filters: + query = query.filter(ProductionRun.mode == filters["mode"]) + + runs = query.order_by(ProductionRun.start_time.desc()).limit(limit).all() + + return [ + { + "run_id": str(run.run_id), + "run_name": run.run_name, + "start_time": run.start_time.isoformat() if run.start_time else None, + "end_time": run.end_time.isoformat() if run.end_time else None, + "mode": run.mode, + "total_steps": run.total_steps, + "simulation_time": run.simulation_time, + "cumulative_reward": run.cumulative_reward, + "status": run.status, + "metadata": run.run_metadata, + } + for run in runs + ] + finally: + session.close() + + def get_run_count(self) -> int: + """Get total number of production runs. + + Returns: + Number of runs in database + """ + session = self.SessionLocal() + try: + return session.query(ProductionRun).count() + finally: + session.close() + + def execute_sql(self, query: str) -> list[dict]: + """Execute a read-only SQL query. + + Args: + query: SQL SELECT query + + Returns: + List of result dicts + + Raises: + ValueError: If query is not a SELECT statement + """ + # Safety check: only allow SELECT + query_upper = query.strip().upper() + if not query_upper.startswith("SELECT"): + raise ValueError("Only SELECT queries are allowed") + + # Block dangerous keywords + dangerous_keywords = ["DROP", "DELETE", "UPDATE", "INSERT", "ALTER", "TRUNCATE", "CREATE"] + for keyword in dangerous_keywords: + if keyword in query_upper: + raise ValueError(f"Query contains forbidden keyword: {keyword}") + + session = self.SessionLocal() + try: + result = session.execute(query) + columns = result.keys() + rows = result.fetchall() + + return [dict(zip(columns, row)) for row in rows] + finally: + session.close() + + # Knowledge Base Operations + + def add_knowledge( + self, + title: str, + content: str, + doc_type: str, + embedding: list[float] | None = None, + metadata: dict | None = None, + ) -> str: + """Add a document to the knowledge base. + + Args: + title: Document title + content: Document content + doc_type: Type of document + embedding: Vector embedding (if available) + metadata: Optional metadata + + Returns: + doc_id as string + """ + session = self.SessionLocal() + try: + doc = KnowledgeBase( + title=title, + content=content, + doc_type=doc_type, + embedding=embedding, + doc_metadata=metadata, + ) + session.add(doc) + session.commit() + doc_id = str(doc.doc_id) + return doc_id + finally: + session.close() + + def search_knowledge( + self, + query_embedding: list[float], + doc_type: str | None = None, + top_k: int = 5, + ) -> list[dict]: + """Search knowledge base using vector similarity. + + Args: + query_embedding: Query vector embedding + doc_type: Optional document type filter + top_k: Number of results to return + + Returns: + List of matching documents with similarity scores + """ + session = self.SessionLocal() + try: + query = session.query(KnowledgeBase) + + if doc_type: + query = query.filter(KnowledgeBase.doc_type == doc_type) + + docs = query.all() + + # Compute cosine similarity (simple implementation) + def cosine_similarity(a: list[float], b: list[float]) -> float: + if not a or not b: + return 0.0 + dot_product = sum(x * y for x, y in zip(a, b)) + mag_a = sum(x * x for x in a) ** 0.5 + mag_b = sum(x * x for x in b) ** 0.5 + if mag_a == 0 or mag_b == 0: + return 0.0 + return dot_product / (mag_a * mag_b) + + results = [] + for doc in docs: + if doc.embedding: + similarity = cosine_similarity(query_embedding, doc.embedding) + results.append({ + "doc_id": str(doc.doc_id), + "title": doc.title, + "content": doc.content, + "doc_type": doc.doc_type, + "similarity": similarity, + "metadata": doc.doc_metadata, + }) + + # Sort by similarity and return top-k + results.sort(key=lambda x: x["similarity"], reverse=True) + return results[:top_k] + finally: + session.close() + + # ===================================================================== + # Phase 3: ML Training Data Retrieval + # ===================================================================== + + def get_sensor_readings(self, limit: int = 1000, machine_id: str | None = None) -> list[dict]: + """Get sensor readings for ML model training. + + Args: + limit: Maximum number of readings to return + machine_id: Optional filter by machine ID + + Returns: + List of sensor reading dicts + """ + session = self.Session() + try: + query = session.query(SensorReading) + + if machine_id: + query = query.filter(SensorReading.machine_id == machine_id) + + readings = query.order_by(SensorReading.timestamp.desc()).limit(limit).all() + + return [ + { + "machine_id": r.machine_id, + "temperature": r.temperature, + "vibration": r.vibration, + "speed": r.speed, + "health_score": r.health_score, + "wear_level": r.wear_level, + "production_output": r.production_output, + "timestamp": r.timestamp.isoformat() if r.timestamp else None, + "failed": r.health_score < 30 if r.health_score else False, + } + for r in readings + ] + finally: + session.close() + + def get_production_units(self, limit: int = 1000, machine_id: str | None = None) -> list[dict]: + """Get production units for quality prediction training. + + Args: + limit: Maximum number of units to return + machine_id: Optional filter by machine ID + + Returns: + List of production unit dicts + """ + session = self.Session() + try: + query = session.query(ProductionUnit) + + if machine_id: + query = query.filter(ProductionUnit.machine_id == machine_id) + + units = query.order_by(ProductionUnit.completed_at.desc()).limit(limit).all() + + return [ + { + "machine_id": u.machine_id, + "speed": u.unit_metadata.get("speed", 0) if u.unit_metadata else 0, + "temperature": u.unit_metadata.get("temperature", 0) if u.unit_metadata else 0, + "vibration": u.unit_metadata.get("vibration", 0) if u.unit_metadata else 0, + "wear_level": u.unit_metadata.get("wear_level", 0) if u.unit_metadata else 0, + "quality": u.quality_score, + } + for u in units + ] + finally: + session.close() diff --git a/src/envs/cognitive_manufacturing/server/embeddings.py b/src/envs/cognitive_manufacturing/server/embeddings.py new file mode 100644 index 00000000..85780168 --- /dev/null +++ b/src/envs/cognitive_manufacturing/server/embeddings.py @@ -0,0 +1,68 @@ +"""Text embedding service for vector search in knowledge base.""" + +from typing import TYPE_CHECKING + +try: + from sentence_transformers import SentenceTransformer + SENTENCE_TRANSFORMERS_AVAILABLE = True +except ImportError: + SENTENCE_TRANSFORMERS_AVAILABLE = False + + +class EmbeddingService: + """Handles text embedding for semantic search.""" + + def __init__(self, model_name: str = "all-MiniLM-L6-v2"): + """Initialize embedding service. + + Args: + model_name: Name of sentence-transformers model to use + Default: 'all-MiniLM-L6-v2' (384-dim, fast, good quality) + """ + if not SENTENCE_TRANSFORMERS_AVAILABLE: + raise ImportError( + "sentence-transformers is required for embedding functionality. " + "Install with: pip install sentence-transformers" + ) + + self.model = SentenceTransformer(model_name) + self.model_name = model_name + self.embedding_dim = self.model.get_sentence_embedding_dimension() + + def embed_text(self, text: str) -> list[float]: + """Generate embedding for a single text. + + Args: + text: Text to embed + + Returns: + List of floats representing the embedding vector + """ + embedding = self.model.encode(text, convert_to_numpy=True) + return embedding.tolist() + + def embed_batch(self, texts: list[str], batch_size: int = 32) -> list[list[float]]: + """Generate embeddings for multiple texts. + + Args: + texts: List of texts to embed + batch_size: Batch size for encoding + + Returns: + List of embedding vectors + """ + embeddings = self.model.encode( + texts, + batch_size=batch_size, + convert_to_numpy=True, + show_progress_bar=False, + ) + return [emb.tolist() for emb in embeddings] + + def get_dimension(self) -> int: + """Get the dimensionality of embeddings. + + Returns: + Embedding dimension (e.g., 384 for all-MiniLM-L6-v2) + """ + return self.embedding_dim diff --git a/src/envs/cognitive_manufacturing/server/environment.py b/src/envs/cognitive_manufacturing/server/environment.py new file mode 100644 index 00000000..480e5a93 --- /dev/null +++ b/src/envs/cognitive_manufacturing/server/environment.py @@ -0,0 +1,496 @@ +"""Core environment implementation for cognitive manufacturing. + +This module implements the OpenEnv-compliant environment that orchestrates: +- Tool execution +- Simulator stepping +- Reward computation +- State management +""" + +from __future__ import annotations +from typing import Any +from core.env_server import Environment +from ..models import ( + ManufacturingAction, + ManufacturingObservation, + ManufacturingState, + MachineStatus, +) +from ..tools import ( + # Phase 0 + ReadSensorsTool, + CheckHealthTool, + AdjustSpeedTool, + ScheduleMaintenanceTool, + SendAlertTool, + # Phase 1 + GetLineStatusTool, + TransferMaterialTool, + OptimizeLineSpeedTool, + # Phase 2 + SaveProductionDataTool, + QueryProductionHistoryTool, + ExecuteSQLTool, + ExportToCSVTool, + ImportFromCSVTool, + SearchKnowledgeTool, + AddKnowledgeTool, + # Phase 3 + PredictMaintenanceTool, + DetectAnomalyTool, + PredictQualityTool, + OptimizeWithRLTool, + ForecastDemandTool, + # Phase 4 + InspectProductTool, + RecordDefectTool, + UpdateQCThresholdsTool, + CheckInventoryTool, + OrderMaterialsTool, + UpdateStockLevelsTool, + MonitorEnergyUsageTool, + SetPowerModeTool, + SimulateScenarioTool, + OptimizeProductionScheduleTool, +) +from .simulator import SimulatedMachine +from .production_line import ProductionLineSimulator +from .rewards import RewardCalculator, compute_cumulative_metrics + + +class CognitiveManufacturingEnvironment(Environment): + """OpenEnv-compliant environment for cognitive manufacturing scenarios. + + This environment can operate in multiple modes: + 1. Single Machine Mode (Phase 0): One machine with 5 tools + 2. Production Line Mode (Phase 1): 4-machine production line with 8 tools + 3. Database Mode (Phase 2): Add 7 data tools (15 tools total) + 4. ML Mode (Phase 3): Add 5 ML-powered tools (20 tools total) + 5. Advanced Management (Phase 4): Add 10 advanced tools (30 tools total) + + Available tools (Phase 0): + 1. ReadSensors - Get current sensor readings + 2. CheckHealth - Perform health diagnostics + 3. AdjustSpeed - Change machine operating speed + 4. ScheduleMaintenance - Schedule or perform maintenance + 5. SendAlert - Send notifications to operators + + Additional tools (Phase 1): + 6. GetLineStatus - Get status of entire production line + 7. TransferMaterial - Manually transfer material between machines + 8. OptimizeLineSpeed - Optimize all machine speeds + + Additional tools (Phase 2): + 9. SaveProductionData - Save production run to database + 10. QueryProductionHistory - Query historical data + 11. ExecuteSQL - Execute custom SQL queries + 12. ExportToCSV - Export data to CSV + 13. ImportFromCSV - Import data from CSV + 14. SearchKnowledge - Semantic search in knowledge base + 15. AddKnowledge - Add documents to knowledge base + + Additional tools (Phase 3): + 16. PredictMaintenance - Predict when maintenance will be needed + 17. DetectAnomaly - Detect abnormal sensor patterns + 18. PredictQuality - Predict product quality + 19. OptimizeWithRL - Use reinforcement learning for optimization + 20. ForecastDemand - Forecast future production demand + + Additional tools (Phase 4): + 21. InspectProduct - Detailed quality inspection + 22. RecordDefect - Log defects and issues + 23. UpdateQCThresholds - Adjust quality control parameters + 24. CheckInventory - Check material inventory levels + 25. OrderMaterials - Order raw materials + 26. UpdateStockLevels - Update inventory records + 27. MonitorEnergyUsage - Track power consumption + 28. SetPowerMode - Adjust machine power modes + 29. SimulateScenario - Run what-if simulations + 30. OptimizeProductionSchedule - Optimize production schedule + + The environment tracks multi-objective rewards: + - Safety (highest priority) + - Throughput (production efficiency) + - Quality (defect minimization) + - Cost (operational expenses) + - Sustainability (energy efficiency) + """ + + def __init__( + self, + machine_id: str = "M1", + timestep: float = 0.1, + multi_machine: bool = False, + enable_database: bool = False, + db_connection_string: str | None = None, + enable_ml: bool = False, + ): + """Initialize the manufacturing environment. + + Args: + machine_id: Identifier for the simulated machine (single machine mode) + timestep: Simulation timestep in hours (default 0.1 = 6 minutes) + multi_machine: If True, use production line mode with 4 machines (Phase 1) + enable_database: If True, enable database and Phase 2 tools + db_connection_string: PostgreSQL connection string (if enable_database=True) + Example: "postgresql://user:pass@localhost/dbname" + enable_ml: If True, enable ML features and Phase 3 tools (requires enable_database=True) + """ + super().__init__() + self.machine_id = machine_id + self.timestep = timestep + self.multi_machine = multi_machine + self.enable_database = enable_database + self.enable_ml = enable_ml + + # Initialize simulator (single or multi-machine) + if multi_machine: + self.production_line = ProductionLineSimulator() + self.simulator_machine = self.production_line.machines["M1"] # For backward compatibility + else: + self.simulator_machine = SimulatedMachine(machine_id=machine_id) + self.production_line = None + + # Initialize reward calculator + self.reward_calculator = RewardCalculator() + + # Initialize Phase 2 services if database enabled + self.db_manager = None + self.embedding_service = None + self.csv_service = None + self.current_run_id = None + + if enable_database: + # Import Phase 2 services + try: + from .database import DatabaseManager + from .embeddings import EmbeddingService + from .csv_service import CSVService + + # Use default SQLite if no connection string provided + if db_connection_string is None: + import os + db_path = os.path.join(os.getcwd(), "data", "cognitive_manufacturing.db") + os.makedirs(os.path.dirname(db_path), exist_ok=True) + db_connection_string = f"sqlite:///{db_path}" + + self.db_manager = DatabaseManager(db_connection_string) + self.db_manager.create_tables() + + self.embedding_service = EmbeddingService() + self.csv_service = CSVService(export_dir="data/exports") + + except ImportError as e: + raise ImportError( + f"Phase 2 dependencies not installed: {e}\n" + "Install with: pip install sqlalchemy sentence-transformers pandas" + ) + + # Initialize Phase 3 services if ML enabled + self.ml_service = None + + if enable_ml: + if not enable_database: + raise ValueError("ML features require database (enable_database=True)") + + # Import Phase 3 services + try: + from .ml_models import MLModelsService + + self.ml_service = MLModelsService() + + # Train models if database has sufficient data + if self.db_manager.get_run_count() > 10: + # Train maintenance model from sensor readings + sensor_data = self.db_manager.get_sensor_readings(limit=1000) + if len(sensor_data) >= 10: + self.ml_service.train_maintenance_model(sensor_data) + + # Train anomaly detector from normal operation data + normal_data = [s for s in sensor_data if s.get('health_score', 100) > 70] + if len(normal_data) >= 20: + self.ml_service.train_anomaly_detector(normal_data) + + # Train quality predictor from production units + production_data = self.db_manager.get_production_units(limit=1000) + if len(production_data) >= 10: + self.ml_service.train_quality_predictor(production_data) + + except ImportError as e: + raise ImportError( + f"Phase 3 dependencies not installed: {e}\n" + "Install with: pip install scikit-learn numpy scipy" + ) + + # Initialize tools (Phase 0 tools always available) + self.tools = { + "ReadSensors": ReadSensorsTool(), + "CheckHealth": CheckHealthTool(), + "AdjustSpeed": AdjustSpeedTool(), + "ScheduleMaintenance": ScheduleMaintenanceTool(), + "SendAlert": SendAlertTool(), + } + + # Add Phase 1 tools if in multi-machine mode + if multi_machine: + self.tools.update({ + "GetLineStatus": GetLineStatusTool(), + "TransferMaterial": TransferMaterialTool(), + "OptimizeLineSpeed": OptimizeLineSpeedTool(), + }) + + # Add Phase 2 tools if database enabled + if enable_database: + self.tools.update({ + "SaveProductionData": SaveProductionDataTool(), + "QueryProductionHistory": QueryProductionHistoryTool(), + "ExecuteSQL": ExecuteSQLTool(), + "ExportToCSV": ExportToCSVTool(), + "ImportFromCSV": ImportFromCSVTool(), + "SearchKnowledge": SearchKnowledgeTool(), + "AddKnowledge": AddKnowledgeTool(), + }) + + # Add Phase 3 tools if ML enabled + if enable_ml: + self.tools.update({ + "PredictMaintenance": PredictMaintenanceTool(), + "DetectAnomaly": DetectAnomalyTool(), + "PredictQuality": PredictQualityTool(), + "OptimizeWithRL": OptimizeWithRLTool(), + "ForecastDemand": ForecastDemandTool(), + }) + + # Add Phase 4 tools (always available) + self.tools.update({ + # Quality Management + "InspectProduct": InspectProductTool(), + "RecordDefect": RecordDefectTool(), + "UpdateQCThresholds": UpdateQCThresholdsTool(), + # Inventory Management + "CheckInventory": CheckInventoryTool(), + "OrderMaterials": OrderMaterialsTool(), + "UpdateStockLevels": UpdateStockLevelsTool(), + # Energy Management + "MonitorEnergyUsage": MonitorEnergyUsageTool(), + "SetPowerMode": SetPowerModeTool(), + # Advanced Optimization + "SimulateScenario": SimulateScenarioTool(), + "OptimizeProductionSchedule": OptimizeProductionScheduleTool(), + }) + + # Initialize state + self._state = ManufacturingState(simulation_time=0.0) + self._cumulative_reward = 0.0 + self._last_reward = 0.0 + self._alerts = [] + self._done = False + + def reset(self) -> ManufacturingObservation: + """Reset the environment to initial state. + + Returns: + Initial observation with machine status + """ + # Reset simulator (single or multi-machine) + if self.multi_machine: + self.production_line = ProductionLineSimulator() + self.simulator_machine = self.production_line.machines["M1"] # For backward compatibility + else: + self.simulator_machine = SimulatedMachine(machine_id=self.machine_id) + + # Reset state + self._state = ManufacturingState(simulation_time=0.0) + self._cumulative_reward = 0.0 + self._last_reward = 0.0 + self._alerts = [] + self._done = False + + # Return initial observation + machine_status = MachineStatus( + machine_id=self.simulator_machine.machine_id, + status=self.simulator_machine.status, + temperature=round(self.simulator_machine.temperature, 2), + vibration=round(self.simulator_machine.vibration, 3), + speed=round(self.simulator_machine.speed, 1), + health_score=round(self.simulator_machine.health_score, 1), + wear_level=round(self.simulator_machine.wear_level, 3), + production_output=round(self.simulator_machine.production_output, 2), + ) + + mode_message = "production line" if self.multi_machine else "single machine" + return ManufacturingObservation( + tool_result={"message": f"Environment reset ({mode_message} mode)", "initial_status": "ready"}, + machine_status=machine_status, + alerts=[], + simulation_time=0.0, + ) + + def step(self, action: ManufacturingAction) -> tuple[ManufacturingObservation, float, bool, dict[str, Any]]: + """Execute one environment step with the given action. + + Args: + action: Tool-calling action to execute + + Returns: + Tuple of (observation, reward, done, info) + """ + # Execute tool + tool_result = self._execute_tool(action) + + # Step simulator forward in time + if self.multi_machine: + # Step entire production line + line_result = self.production_line.step(self.timestep) + sim_result = line_result + # Update metrics for production line + self._state.total_produced = self.production_line.metrics.total_produced + else: + # Step single machine + sim_result = self.simulator_machine.step(self.timestep) + + # Compute reward + reward, reward_breakdown = self.reward_calculator.compute_reward( + self.simulator_machine, + self._state, + self.timestep, + ) + + # Update state + self._state.simulation_time += self.timestep + self._state.step_count += 1 + self._last_reward = reward + self._cumulative_reward += reward + + # Check termination conditions + self._done = self._check_done() + + # Create machine status snapshot (M1 for multi-machine mode) + machine_status = MachineStatus( + machine_id=self.simulator_machine.machine_id, + status=self.simulator_machine.status, + temperature=round(self.simulator_machine.temperature, 2), + vibration=round(self.simulator_machine.vibration, 3), + speed=round(self.simulator_machine.speed, 1), + health_score=round(self.simulator_machine.health_score, 1), + wear_level=round(self.simulator_machine.wear_level, 3), + production_output=round(self.simulator_machine.production_output, 2), + ) + + # Create observation + observation = ManufacturingObservation( + tool_result=tool_result, + machine_status=machine_status, + alerts=self._alerts[-5:], # Last 5 alerts + simulation_time=round(self._state.simulation_time, 2), + ) + + # Additional info + info = { + "reward_breakdown": reward_breakdown, + "simulator_events": sim_result, + "cumulative_metrics": { + "total_reward": self._cumulative_reward, + "simulation_time": self._state.simulation_time, + "avg_reward_per_hour": self._cumulative_reward / max(self._state.simulation_time, 1.0), + "total_alerts": len(self._alerts), + "critical_alerts": sum(1 for alert in self._alerts if alert.severity == "critical"), + }, + } + + # Add production line metrics if in multi-machine mode + if self.multi_machine: + info["production_line"] = { + "total_produced": self.production_line.metrics.total_produced, + "throughput_rate": self.production_line.metrics.throughput_rate, + "qc_pass_rate": self.production_line.metrics.qc_pass_rate, + "line_efficiency": self.production_line.metrics.line_efficiency, + "bottleneck": self.production_line.metrics.bottleneck_machine, + "finished_products": len(self.production_line.finished_products), + } + + return observation, reward, self._done, info + + def state(self) -> dict[str, Any]: + """Get current environment state for inspection. + + Returns: + Dictionary containing full environment state + """ + return { + "simulation_time": self._state.simulation_time, + "step_count": self._state.step_count, + "cumulative_reward": self._cumulative_reward, + "last_reward": self._last_reward, + "done": self._done, + "machine": { + "machine_id": self.simulator_machine.machine_id, + "status": self.simulator_machine.status, + "temperature": round(self.simulator_machine.temperature, 2), + "vibration": round(self.simulator_machine.vibration, 3), + "speed": round(self.simulator_machine.speed, 1), + "health_score": round(self.simulator_machine.health_score, 1), + "wear_level": round(self.simulator_machine.wear_level, 3), + "production_output": round(self.simulator_machine.production_output, 2), + }, + "alerts": [ + { + "alert_id": alert.alert_id, + "severity": alert.severity, + "message": alert.message, + "category": alert.category, + "timestamp": alert.timestamp, + } + for alert in self._alerts[-10:] # Last 10 alerts + ], + "available_tools": list(self.tools.keys()), + } + + def _execute_tool(self, action: ManufacturingAction) -> dict[str, Any]: + """Execute the requested tool. + + Args: + action: Action specifying tool and parameters + + Returns: + Tool execution result as dictionary + """ + tool_name = action.tool_name + parameters = action.parameters + + # Check if tool exists + if tool_name not in self.tools: + return { + "success": False, + "error": f"Unknown tool: {tool_name}. Available tools: {list(self.tools.keys())}", + } + + # Execute tool + tool = self.tools[tool_name] + result = tool.execute(parameters, self) + + # ToolResult is already a dict, return it directly + return result + + def _check_done(self) -> bool: + """Check if episode should terminate. + + Returns: + True if episode is done, False otherwise + """ + # Episode ends if: + # 1. Machine has failed + # 2. Simulation time exceeds 24 hours + # 3. Too many critical alerts (safety concern) + + if self.simulator_machine.status == "failed": + return True + + if self._state.simulation_time >= 24.0: + return True + + critical_alerts = sum(1 for alert in self._alerts if alert.severity == "critical") + if critical_alerts >= 10: + return True + + return False diff --git a/src/envs/cognitive_manufacturing/server/ml_models.py b/src/envs/cognitive_manufacturing/server/ml_models.py new file mode 100644 index 00000000..f06d00ed --- /dev/null +++ b/src/envs/cognitive_manufacturing/server/ml_models.py @@ -0,0 +1,517 @@ +"""Machine learning models service for predictive analytics.""" + +from __future__ import annotations +import numpy as np +from typing import Any, TYPE_CHECKING +from collections import deque +import json + +if TYPE_CHECKING: + from .database import DatabaseManager + +try: + from sklearn.ensemble import RandomForestClassifier, IsolationForest + from sklearn.linear_model import LinearRegression + from sklearn.preprocessing import StandardScaler + SKLEARN_AVAILABLE = True +except ImportError: + SKLEARN_AVAILABLE = False + + +class MLModelsService: + """Manages machine learning models for predictive analytics.""" + + def __init__(self): + """Initialize ML models service.""" + if not SKLEARN_AVAILABLE: + raise ImportError( + "scikit-learn is required for ML features. " + "Install with: pip install scikit-learn numpy scipy" + ) + + # Models + self.maintenance_model = None + self.anomaly_detector = None + self.quality_predictor = None + self.scaler = StandardScaler() + + # RL Agent (simple Q-learning) + self.q_table = {} + self.learning_rate = 0.1 + self.discount_factor = 0.95 + self.exploration_rate = 0.2 + + # Historical data for forecasting + self.demand_history = deque(maxlen=1000) + + # Training status + self.models_trained = False + + # ===================================================================== + # Predictive Maintenance + # ===================================================================== + + def train_maintenance_model(self, sensor_data: list[dict]): + """Train predictive maintenance model. + + Args: + sensor_data: List of sensor readings with labels + Each dict should have: temperature, vibration, wear_level, health_score, failed (bool) + """ + if len(sensor_data) < 10: + # Not enough data to train + return False + + # Extract features and labels + X = [] + y = [] + + for reading in sensor_data: + features = [ + reading.get('temperature', 0), + reading.get('vibration', 0), + reading.get('wear_level', 0), + reading.get('health_score', 100), + reading.get('speed', 0), + ] + X.append(features) + y.append(1 if reading.get('failed', False) else 0) + + X = np.array(X) + y = np.array(y) + + # Train scaler + self.scaler.fit(X) + X_scaled = self.scaler.transform(X) + + # Train Random Forest + self.maintenance_model = RandomForestClassifier( + n_estimators=50, + max_depth=10, + random_state=42 + ) + self.maintenance_model.fit(X_scaled, y) + + return True + + def predict_maintenance_need( + self, + temperature: float, + vibration: float, + wear_level: float, + health_score: float, + speed: float, + ) -> dict: + """Predict if maintenance is needed. + + Args: + temperature: Current temperature + vibration: Current vibration level + wear_level: Current wear level + health_score: Current health score + speed: Current speed + + Returns: + Dict with prediction results + """ + if self.maintenance_model is None: + # Model not trained, use heuristics + maintenance_needed = ( + health_score < 60 or + wear_level > 0.7 or + temperature > 80 + ) + return { + "maintenance_needed": maintenance_needed, + "probability": 0.9 if maintenance_needed else 0.1, + "hours_until_failure": 12.0 if maintenance_needed else 100.0, + "confidence": 0.5, + "method": "heuristic" + } + + # Use trained model + features = np.array([[temperature, vibration, wear_level, health_score, speed]]) + features_scaled = self.scaler.transform(features) + + probability = self.maintenance_model.predict_proba(features_scaled)[0][1] + prediction = probability > 0.5 + + # Estimate hours until failure based on wear rate and health + hours_until_failure = max(1.0, (health_score / max(wear_level * 100, 1)) * 10) + + return { + "maintenance_needed": bool(prediction), + "probability": float(probability), + "hours_until_failure": float(hours_until_failure), + "confidence": float(max(probability, 1 - probability)), + "method": "random_forest" + } + + # ===================================================================== + # Anomaly Detection + # ===================================================================== + + def train_anomaly_detector(self, normal_data: list[dict]): + """Train anomaly detection model. + + Args: + normal_data: List of sensor readings during normal operation + """ + if len(normal_data) < 20: + return False + + # Extract features + X = [] + for reading in normal_data: + features = [ + reading.get('temperature', 0), + reading.get('vibration', 0), + reading.get('speed', 0), + reading.get('production_output', 0), + ] + X.append(features) + + X = np.array(X) + + # Train Isolation Forest + self.anomaly_detector = IsolationForest( + contamination=0.1, # Expected proportion of outliers + random_state=42 + ) + self.anomaly_detector.fit(X) + + return True + + def detect_anomalies(self, sensor_readings: list[dict]) -> dict: + """Detect anomalies in sensor patterns. + + Args: + sensor_readings: Recent sensor readings to analyze + + Returns: + Dict with anomaly detection results + """ + if len(sensor_readings) == 0: + return { + "anomaly_detected": False, + "anomaly_score": 0.0, + "anomalous_sensors": [], + "severity": "none" + } + + # Extract features + X = [] + for reading in sensor_readings: + features = [ + reading.get('temperature', 0), + reading.get('vibration', 0), + reading.get('speed', 0), + reading.get('production_output', 0), + ] + X.append(features) + + X = np.array(X) + + if self.anomaly_detector is not None: + # Use trained model + predictions = self.anomaly_detector.predict(X) + scores = self.anomaly_detector.score_samples(X) + + anomaly_count = np.sum(predictions == -1) + anomaly_score = float(anomaly_count / len(predictions)) + + # Identify which sensors are anomalous + anomalous_sensors = [] + if anomaly_score > 0.3: + # Check each sensor individually + recent = sensor_readings[-1] + historical_avg = { + 'temperature': np.mean([r.get('temperature', 0) for r in sensor_readings[:-1]]), + 'vibration': np.mean([r.get('vibration', 0) for r in sensor_readings[:-1]]), + 'speed': np.mean([r.get('speed', 0) for r in sensor_readings[:-1]]), + } + + for sensor, avg in historical_avg.items(): + current = recent.get(sensor, 0) + if abs(current - avg) / max(avg, 1) > 0.3: # 30% deviation + anomalous_sensors.append(sensor) + + else: + # Fallback: use z-score method + anomaly_score = 0.0 + anomalous_sensors = [] + + for sensor in ['temperature', 'vibration', 'speed']: + values = [r.get(sensor, 0) for r in sensor_readings] + mean = np.mean(values) + std = np.std(values) + + if std > 0: + z_score = abs((values[-1] - mean) / std) + if z_score > 2.5: # 2.5 standard deviations + anomaly_score = max(anomaly_score, min(z_score / 3, 1.0)) + anomalous_sensors.append(sensor) + + # Determine severity + if anomaly_score > 0.7: + severity = "high" + elif anomaly_score > 0.4: + severity = "medium" + else: + severity = "low" + + return { + "anomaly_detected": anomaly_score > 0.3, + "anomaly_score": float(anomaly_score), + "anomalous_sensors": anomalous_sensors, + "severity": severity + } + + # ===================================================================== + # Quality Prediction + # ===================================================================== + + def train_quality_predictor(self, production_data: list[dict]): + """Train quality prediction model. + + Args: + production_data: List of production units with quality scores + Each dict: speed, temperature, vibration, quality (0-1) + """ + if len(production_data) < 10: + return False + + # Extract features and target + X = [] + y = [] + + for unit in production_data: + features = [ + unit.get('speed', 0), + unit.get('temperature', 0), + unit.get('vibration', 0), + unit.get('wear_level', 0), + ] + X.append(features) + y.append(unit.get('quality', 1.0)) + + X = np.array(X) + y = np.array(y) + + # Train Linear Regression + self.quality_predictor = LinearRegression() + self.quality_predictor.fit(X, y) + + return True + + def predict_quality( + self, + speed: float, + temperature: float, + vibration: float, + wear_level: float, + ) -> dict: + """Predict product quality. + + Args: + speed: Machine speed + temperature: Operating temperature + vibration: Vibration level + wear_level: Machine wear level + + Returns: + Dict with quality prediction + """ + if self.quality_predictor is None: + # Heuristic quality model + quality = 1.0 + quality -= min(temperature / 100, 0.2) # High temp reduces quality + quality -= min(vibration / 0.5, 0.2) # High vibration reduces quality + quality -= min(wear_level, 0.2) # Wear reduces quality + quality = max(0, min(1, quality)) + + return { + "predicted_quality": float(quality), + "confidence_interval": [quality - 0.1, quality + 0.1], + "pass_probability": float(quality), + "method": "heuristic" + } + + # Use trained model + features = np.array([[speed, temperature, vibration, wear_level]]) + predicted_quality = float(self.quality_predictor.predict(features)[0]) + predicted_quality = max(0, min(1, predicted_quality)) + + # Simple confidence interval (would use prediction intervals in production) + confidence_interval = [ + max(0, predicted_quality - 0.05), + min(1, predicted_quality + 0.05) + ] + + return { + "predicted_quality": predicted_quality, + "confidence_interval": confidence_interval, + "pass_probability": predicted_quality, + "method": "linear_regression" + } + + # ===================================================================== + # RL Optimization + # ===================================================================== + + def get_state_key(self, state: dict) -> str: + """Convert state dict to string key for Q-table.""" + # Discretize state for Q-learning + health = int(state.get('health_score', 100) / 20) * 20 # Buckets: 0, 20, 40, 60, 80, 100 + temp = int(state.get('temperature', 20) / 10) * 10 # Buckets: 20, 30, 40, ... + speed = int(state.get('speed', 0) / 25) * 25 # Buckets: 0, 25, 50, 75, 100 + + return f"h{health}_t{temp}_s{speed}" + + def select_action_rl( + self, + state: dict, + available_actions: list[dict], + learning_mode: str = "exploit" + ) -> dict: + """Select action using RL policy. + + Args: + state: Current environment state + available_actions: List of possible actions + learning_mode: "exploit" (use best action) or "explore" (try new actions) + + Returns: + Selected action with expected reward + """ + state_key = self.get_state_key(state) + + # Initialize Q-values for this state if not seen before + if state_key not in self.q_table: + self.q_table[state_key] = { + json.dumps(action): 0.0 for action in available_actions + } + + # Epsilon-greedy exploration + if learning_mode == "explore" and np.random.random() < self.exploration_rate: + # Explore: random action + action = np.random.choice(available_actions) + action_key = json.dumps(action) + expected_reward = self.q_table[state_key].get(action_key, 0.0) + else: + # Exploit: best known action + q_values = self.q_table[state_key] + best_action_key = max(q_values, key=q_values.get) + action = json.loads(best_action_key) + expected_reward = q_values[best_action_key] + + return { + "action": action, + "expected_reward": float(expected_reward), + "confidence": 0.8 if learning_mode == "exploit" else 0.3 + } + + def update_q_value(self, state: dict, action: dict, reward: float, next_state: dict): + """Update Q-value after receiving reward (Q-learning update). + + Args: + state: Previous state + action: Action taken + reward: Reward received + next_state: New state after action + """ + state_key = self.get_state_key(state) + next_state_key = self.get_state_key(next_state) + action_key = json.dumps(action) + + # Initialize if needed + if state_key not in self.q_table: + self.q_table[state_key] = {} + if action_key not in self.q_table[state_key]: + self.q_table[state_key][action_key] = 0.0 + + # Q-learning update: Q(s,a) = Q(s,a) + α[r + γ max(Q(s',a')) - Q(s,a)] + current_q = self.q_table[state_key][action_key] + + # Max Q-value for next state + if next_state_key in self.q_table and self.q_table[next_state_key]: + max_next_q = max(self.q_table[next_state_key].values()) + else: + max_next_q = 0.0 + + # Update + new_q = current_q + self.learning_rate * ( + reward + self.discount_factor * max_next_q - current_q + ) + self.q_table[state_key][action_key] = new_q + + # ===================================================================== + # Demand Forecasting + # ===================================================================== + + def update_demand_history(self, production_volume: int, timestamp: float): + """Add data point to demand history.""" + self.demand_history.append({ + 'timestamp': timestamp, + 'volume': production_volume + }) + + def forecast_demand(self, horizon: int = 24) -> list[dict]: + """Forecast future demand. + + Args: + horizon: Number of hours to forecast + + Returns: + List of forecasts for each hour + """ + if len(self.demand_history) < 10: + # Not enough history, return simple projection + avg_demand = 100 # Default + if len(self.demand_history) > 0: + avg_demand = np.mean([d['volume'] for d in self.demand_history]) + + return [ + { + "hour": h, + "demand": float(avg_demand), + "lower_bound": float(avg_demand * 0.8), + "upper_bound": float(avg_demand * 1.2), + } + for h in range(1, horizon + 1) + ] + + # Simple exponential smoothing + values = [d['volume'] for d in self.demand_history] + alpha = 0.3 # Smoothing factor + + # Calculate smoothed values + smoothed = [values[0]] + for v in values[1:]: + smoothed.append(alpha * v + (1 - alpha) * smoothed[-1]) + + last_smoothed = smoothed[-1] + + # Detect trend + recent_values = values[-min(20, len(values)):] + trend = np.mean(np.diff(recent_values)) + + # Generate forecast + forecast = [] + for h in range(1, horizon + 1): + point_forecast = last_smoothed + trend * h + point_forecast = max(0, point_forecast) # Non-negative + + # Simple confidence interval + std = np.std(values[-min(50, len(values)):]) + lower = max(0, point_forecast - 1.96 * std) + upper = point_forecast + 1.96 * std + + forecast.append({ + "hour": h, + "demand": float(point_forecast), + "lower_bound": float(lower), + "upper_bound": float(upper), + }) + + return forecast diff --git a/src/envs/cognitive_manufacturing/server/production_line.py b/src/envs/cognitive_manufacturing/server/production_line.py new file mode 100644 index 00000000..e1c30128 --- /dev/null +++ b/src/envs/cognitive_manufacturing/server/production_line.py @@ -0,0 +1,413 @@ +""" +Production Line Simulator - Phase 1 + +Simulates a 4-machine production line with material flow, buffers, and dependencies. + +Architecture: + [M1: Prep] → Buffer → [M2: Assembly] → Buffer → [M3: Finishing] → Buffer → [M4: QC] +""" + +import uuid +from dataclasses import dataclass, field +from typing import Literal + +from .simulator import SimulatedMachine +from ..models import ProductUnit, Buffer, LineMetrics + + +class ProductionLineSimulator: + """ + Simulates a 4-machine production line with material flow. + + Machines: + M1 (Prep): Prepares raw materials + M2 (Assembly): Assembles components + M3 (Finishing): Applies finishing/coating + M4 (QC): Quality control inspection + + Flow: + Raw → M1 → Buffer → M2 → Buffer → M3 → Buffer → M4 → Finished + """ + + def __init__(self): + """Initialize production line with 4 machines and 3 buffers.""" + # Create machines + self.machines = { + "M1": SimulatedMachine(machine_id="M1"), + "M2": SimulatedMachine(machine_id="M2"), + "M3": SimulatedMachine(machine_id="M3"), + "M4": SimulatedMachine(machine_id="M4"), + } + + # Create buffers between machines + self.buffers = { + "M1_M2": Buffer(buffer_id="M1_M2", capacity=10), + "M2_M3": Buffer(buffer_id="M2_M3", capacity=10), + "M3_M4": Buffer(buffer_id="M3_M4", capacity=10), + } + + # Line metrics + self.metrics = LineMetrics() + + # Output storage (finished products) + self.finished_products: list[ProductUnit] = [] + + # Raw material supply (unlimited for now) + self.raw_material_available = True + + def step(self, dt: float) -> dict: + """ + Step the entire production line forward in time. + + Process: + 1. Step each machine's physics (temperature, wear, etc.) + 2. Process production at each stage + 3. Move materials between stages + 4. Update buffers + 5. Calculate metrics + + Args: + dt: Time step in hours + + Returns: + Dictionary with step results + """ + results = { + "machines": {}, + "buffers": {}, + "produced": 0, + "defects": 0, + "bottleneck": None, + } + + # Step 1: Update all machine physics + for machine_id, machine in self.machines.items(): + machine_result = machine.step(dt) + results["machines"][machine_id] = machine_result + + # Step 2: Process production at each stage (in reverse order to allow flow) + + # M4 (QC): Inspect and output finished products + if self.machines["M4"].status == "running": + m4_result = self._process_qc(dt) + results["produced"] += m4_result["produced"] + results["defects"] += m4_result["defects"] + + # M3 (Finishing): Process from M2_M3 buffer, output to M3_M4 buffer + if self.machines["M3"].status == "running": + self._process_finishing(dt) + + # M2 (Assembly): Process from M1_M2 buffer, output to M2_M3 buffer + if self.machines["M2"].status == "running": + self._process_assembly(dt) + + # M1 (Prep): Take raw materials, output to M1_M2 buffer + if self.machines["M1"].status == "running": + self._process_prep(dt) + + # Step 3: Update buffer levels in results + for buffer_id, buffer in self.buffers.items(): + results["buffers"][buffer_id] = { + "level": buffer.current_level, + "capacity": buffer.capacity, + "utilization": buffer.current_level / buffer.capacity, + } + + # Step 4: Identify bottleneck + results["bottleneck"] = self._identify_bottleneck() + + # Step 5: Update metrics + self._update_metrics(dt) + + return results + + def _process_prep(self, dt: float): + """M1: Prepare raw materials.""" + machine = self.machines["M1"] + buffer = self.buffers["M1_M2"] + + if not self.raw_material_available: + return + + # Check if buffer has space + if buffer.current_level >= buffer.capacity: + return # Buffer full, M1 blocked + + # Calculate production + production_rate = machine.production_output # units/hour + units_to_produce = int(production_rate * dt) + + for _ in range(units_to_produce): + if buffer.current_level >= buffer.capacity: + break + + # Create new product unit + unit = ProductUnit( + unit_id=str(uuid.uuid4())[:8], + created_at=self.metrics.total_produced, # Use as timestamp + stage="prep", + quality=1.0 - (machine.defect_rate * 0.1), # Slight quality variation + ) + + # Add to buffer + if buffer.add_unit(unit): + machine.units_produced += 1 + + def _process_assembly(self, dt: float): + """M2: Assemble components from buffer.""" + machine = self.machines["M2"] + input_buffer = self.buffers["M1_M2"] + output_buffer = self.buffers["M2_M3"] + + # Check input and output availability + if input_buffer.current_level == 0: + return # No input, M2 starved + + if output_buffer.current_level >= output_buffer.capacity: + return # Output full, M2 blocked + + # Calculate how many we can process + production_rate = machine.production_output + units_to_process = int(production_rate * dt) + units_available = min(units_to_process, input_buffer.current_level) + + for _ in range(units_available): + if output_buffer.current_level >= output_buffer.capacity: + break + + # Take unit from input + units = input_buffer.remove_units(1) + if not units: + break + + unit = units[0] + unit.stage = "assembly" + + # Quality may degrade based on machine condition + if machine.health_score < 80: + unit.quality *= 0.95 + + # Add to output + if output_buffer.add_unit(unit): + machine.units_produced += 1 + + def _process_finishing(self, dt: float): + """M3: Apply finishing/coating.""" + machine = self.machines["M3"] + input_buffer = self.buffers["M2_M3"] + output_buffer = self.buffers["M3_M4"] + + if input_buffer.current_level == 0: + return + + if output_buffer.current_level >= output_buffer.capacity: + return + + production_rate = machine.production_output + units_to_process = int(production_rate * dt) + units_available = min(units_to_process, input_buffer.current_level) + + for _ in range(units_available): + if output_buffer.current_level >= output_buffer.capacity: + break + + units = input_buffer.remove_units(1) + if not units: + break + + unit = units[0] + unit.stage = "finishing" + + # Quality may degrade based on machine condition + if machine.temperature > 85: + unit.quality *= 0.97 + + if output_buffer.add_unit(unit): + machine.units_produced += 1 + + def _process_qc(self, dt: float) -> dict: + """M4: Quality control inspection.""" + machine = self.machines["M4"] + input_buffer = self.buffers["M3_M4"] + + produced = 0 + defects = 0 + + if input_buffer.current_level == 0: + return {"produced": 0, "defects": 0} + + # QC inspects units + production_rate = machine.production_output + units_to_inspect = int(production_rate * dt) + units_available = min(units_to_inspect, input_buffer.current_level) + + for _ in range(units_available): + units = input_buffer.remove_units(1) + if not units: + break + + unit = units[0] + unit.stage = "qc" + + # QC inspection: quality threshold + qc_threshold = 0.85 + if unit.quality >= qc_threshold: + unit.passed_qc = True + self.finished_products.append(unit) + produced += 1 + else: + unit.passed_qc = False + unit.defect_type = "quality_below_threshold" + defects += 1 + # Defective units are scrapped (not added to finished) + + machine.units_produced += 1 + + return {"produced": produced, "defects": defects} + + def _identify_bottleneck(self) -> str: + """Identify which machine is the bottleneck.""" + # Bottleneck is the machine with lowest production capacity + # that's currently running or blocked + + min_rate = float('inf') + bottleneck = None + + for machine_id, machine in self.machines.items(): + if machine.status in ["running", "idle"]: + rate = machine.production_output + if rate < min_rate: + min_rate = rate + bottleneck = machine_id + + return bottleneck + + def _update_metrics(self, dt: float): + """Update line-wide performance metrics.""" + # Update total production + total_produced = sum(m.units_produced for m in self.machines.values()) + self.metrics.total_produced = total_produced + + # QC pass rate + total_finished = len(self.finished_products) + if total_finished > 0: + passed = sum(1 for u in self.finished_products if u.passed_qc) + self.metrics.qc_pass_rate = passed / total_finished + + # Throughput rate (units/hour) - based on M4 output + if dt > 0: + m4_output = self.machines["M4"].units_produced + self.metrics.throughput_rate = m4_output / dt if dt > 0 else 0.0 + + # Line efficiency (how balanced the line is) + speeds = [m.speed for m in self.machines.values() if m.status == "running"] + if speeds: + avg_speed = sum(speeds) / len(speeds) + variance = sum((s - avg_speed) ** 2 for s in speeds) / len(speeds) + # Efficiency is 1.0 when all speeds are equal, lower when imbalanced + self.metrics.line_efficiency = 1.0 / (1.0 + variance / 100.0) + + # Energy consumption + total_energy = sum( + (m.speed / 100.0) * 0.1 * dt # Simple energy model + for m in self.machines.values() + ) + self.metrics.total_energy += total_energy + + # Energy per unit + if total_produced > 0: + self.metrics.energy_per_unit = self.metrics.total_energy / total_produced + + # Bottleneck + self.metrics.bottleneck_machine = self._identify_bottleneck() + + def get_line_status(self) -> dict: + """Get complete status of production line.""" + return { + "machines": { + machine_id: { + "status": m.status, + "speed": m.speed, + "temperature": m.temperature, + "health_score": m.health_score, + "production_output": m.production_output, + "units_produced": m.units_produced, + } + for machine_id, m in self.machines.items() + }, + "buffers": { + buffer_id: { + "level": b.current_level, + "capacity": b.capacity, + "utilization": b.current_level / b.capacity if b.capacity > 0 else 0.0, + } + for buffer_id, b in self.buffers.items() + }, + "metrics": { + "total_produced": self.metrics.total_produced, + "total_defects": self.metrics.total_defects, + "throughput_rate": self.metrics.throughput_rate, + "qc_pass_rate": self.metrics.qc_pass_rate, + "line_efficiency": self.metrics.line_efficiency, + "bottleneck": self.metrics.bottleneck_machine, + "total_energy": self.metrics.total_energy, + "energy_per_unit": self.metrics.energy_per_unit, + "finished_products": len(self.finished_products), + }, + } + + def optimize_line_speed(self, target: str = "balanced") -> dict: + """ + Automatically optimize all machine speeds. + + Args: + target: "throughput", "quality", "energy", or "balanced" + + Returns: + Dictionary with old/new speeds and expected performance + """ + old_speeds = {m_id: m.speed for m_id, m in self.machines.items()} + + if target == "throughput": + # Set all to match the bottleneck's maximum sustainable speed + bottleneck = self._identify_bottleneck() + bottleneck_machine = self.machines[bottleneck] + # Set based on bottleneck's health + target_speed = min(80.0, bottleneck_machine.health_score * 0.8) + + for machine in self.machines.values(): + machine.set_speed(target_speed) + + elif target == "quality": + # Lower speeds for better quality + target_speed = 50.0 + for machine in self.machines.values(): + machine.set_speed(target_speed) + + elif target == "energy": + # Moderate speeds for energy efficiency + target_speed = 40.0 + for machine in self.machines.values(): + machine.set_speed(target_speed) + + else: # balanced + # Balance all machines to same speed + avg_health = sum(m.health_score for m in self.machines.values()) / len(self.machines) + target_speed = min(60.0, avg_health * 0.6) + + for machine in self.machines.values(): + machine.set_speed(target_speed) + + new_speeds = {m_id: m.speed for m_id, m in self.machines.items()} + + # Calculate expected performance + expected_throughput = min(m.production_output for m in self.machines.values()) + expected_energy = sum((m.speed / 100.0) * 0.1 for m in self.machines.values()) + + return { + "old_speeds": old_speeds, + "new_speeds": new_speeds, + "bottleneck": self._identify_bottleneck(), + "expected_throughput": expected_throughput, + "expected_energy": expected_energy, + } diff --git a/src/envs/cognitive_manufacturing/server/rewards.py b/src/envs/cognitive_manufacturing/server/rewards.py new file mode 100644 index 00000000..df5d6f61 --- /dev/null +++ b/src/envs/cognitive_manufacturing/server/rewards.py @@ -0,0 +1,216 @@ +"""Reward computation for cognitive manufacturing environment. + +Multi-objective reward function balancing: +- Safety (highest priority) +- Throughput (production efficiency) +- Quality (defect minimization) +- Cost (operational expenses) +- Sustainability (energy efficiency) +""" + +from typing import TYPE_CHECKING +from dataclasses import dataclass + +if TYPE_CHECKING: + from .simulator import SimulatedMachine + from ..models import ManufacturingState + + +@dataclass +class RewardWeights: + """Configurable weights for multi-objective optimization.""" + + safety: float = 10.0 # Highest priority + throughput: float = 1.0 + quality: float = 2.0 + cost: float = 0.5 + sustainability: float = 0.3 + + +class RewardCalculator: + """Computes multi-objective rewards for manufacturing environment.""" + + def __init__(self, weights: RewardWeights | None = None): + """Initialize reward calculator with configurable weights. + + Args: + weights: Custom weights for objectives. Uses defaults if None. + """ + self.weights = weights or RewardWeights() + + def compute_reward( + self, + machine: "SimulatedMachine", + state: "ManufacturingState", + dt: float, + ) -> tuple[float, dict[str, float]]: + """Compute total reward and component breakdown. + + Args: + machine: The simulated machine + state: Current environment state + dt: Time step duration + + Returns: + Tuple of (total_reward, component_breakdown) + """ + # Compute individual components + safety_reward = self._compute_safety_reward(machine) + throughput_reward = self._compute_throughput_reward(machine, dt) + quality_reward = self._compute_quality_reward(machine, dt) + cost_reward = self._compute_cost_reward(machine, dt) + sustainability_reward = self._compute_sustainability_reward(machine, dt) + + # Weighted sum + total_reward = ( + self.weights.safety * safety_reward + + self.weights.throughput * throughput_reward + + self.weights.quality * quality_reward + + self.weights.cost * cost_reward + + self.weights.sustainability * sustainability_reward + ) + + # Component breakdown for analysis + breakdown = { + "safety": safety_reward, + "throughput": throughput_reward, + "quality": quality_reward, + "cost": cost_reward, + "sustainability": sustainability_reward, + "total": total_reward, + } + + return total_reward, breakdown + + def _compute_safety_reward(self, machine: "SimulatedMachine") -> float: + """Safety reward: penalize dangerous conditions. + + Returns: + Reward in range [-10, 0]. Zero is safe, -10 is critical. + """ + reward = 0.0 + + # Critical penalties + if machine.status == "failed": + reward -= 10.0 # Failure is worst case + elif machine.temperature > 95.0: + reward -= 5.0 # Overheating is dangerous + elif machine.temperature > 85.0: + reward -= 2.0 # High temperature warning + + # Vibration safety + if machine.vibration > 0.8: + reward -= 3.0 # Excessive vibration + elif machine.vibration > 0.6: + reward -= 1.0 # High vibration warning + + # Health-based safety + if machine.health_score < 30.0: + reward -= 4.0 # Critical health + elif machine.health_score < 50.0: + reward -= 1.0 # Low health warning + + return reward + + def _compute_throughput_reward(self, machine: "SimulatedMachine", dt: float) -> float: + """Throughput reward: reward production output. + + Returns: + Reward in range [0, 1] based on production rate. + """ + # Reward is proportional to production output + # Max production is ~10 units/hour at full speed + # Normalize to [0, 1] range + max_production_rate = 10.0 * dt # Max possible in this timestep + actual_production = machine.production_output * dt + + if max_production_rate > 0: + normalized_throughput = min(actual_production / max_production_rate, 1.0) + else: + normalized_throughput = 0.0 + + return normalized_throughput + + def _compute_quality_reward(self, machine: "SimulatedMachine", dt: float) -> float: + """Quality reward: penalize defects. + + Returns: + Reward in range [-1, 0]. Zero defects = 0, many defects = -1. + """ + # Penalize based on defect rate + # Defect rate increases with high temperature and wear + defect_probability = machine.defect_rate + + # Expected defects in this timestep + expected_defects = defect_probability * machine.production_output * dt + + # Normalize penalty (assume max 2 defects per timestep is worst case) + penalty = -min(expected_defects / 2.0, 1.0) + + return penalty + + def _compute_cost_reward(self, machine: "SimulatedMachine", dt: float) -> float: + """Cost reward: penalize operational costs. + + Returns: + Reward in range [-1, 0] based on normalized costs. + """ + # Cost factors: + # 1. Energy consumption (proportional to speed) + # 2. Maintenance costs (proportional to wear) + # 3. Failure costs (high penalty) + + energy_cost = (machine.speed / 100.0) * 0.1 # $0.1 per hour at full speed + maintenance_cost = machine.wear_level * 0.05 # Increases with wear + failure_cost = 1.0 if machine.status == "failed" else 0.0 + + total_cost = (energy_cost + maintenance_cost + failure_cost) * dt + + # Normalize to [-1, 0] range (assume max cost of $1.15 per hour) + max_cost = 1.15 * dt + normalized_cost = -min(total_cost / max_cost, 1.0) if max_cost > 0 else 0.0 + + return normalized_cost + + def _compute_sustainability_reward(self, machine: "SimulatedMachine", dt: float) -> float: + """Sustainability reward: reward energy efficiency. + + Returns: + Reward in range [-1, 1] based on energy efficiency. + """ + # Energy efficiency = production output / energy consumed + # Higher efficiency = better sustainability + + if machine.speed > 0: + energy_consumed = machine.speed / 100.0 # Normalized energy + production_output = machine.production_output + + if energy_consumed > 0: + efficiency = production_output / energy_consumed + # Normalize: efficiency of 10 units/energy = 1.0 reward + normalized_efficiency = min(efficiency / 10.0, 1.0) + else: + normalized_efficiency = 0.0 + else: + # Idle machine: slight penalty for not producing + normalized_efficiency = -0.1 + + return normalized_efficiency + + +def compute_cumulative_metrics(state: "ManufacturingState") -> dict[str, float]: + """Compute cumulative performance metrics. + + Args: + state: Current environment state + + Returns: + Dictionary of cumulative metrics + """ + return { + "total_reward": state.cumulative_reward, + "simulation_time": state.simulation_time, + "avg_reward_per_hour": state.cumulative_reward / max(state.simulation_time, 1.0), + "total_alerts": len(state.alerts), + "critical_alerts": sum(1 for alert in state.alerts if alert.severity == "critical"), + } diff --git a/src/envs/cognitive_manufacturing/server/simulator.py b/src/envs/cognitive_manufacturing/server/simulator.py new file mode 100644 index 00000000..1e313baf --- /dev/null +++ b/src/envs/cognitive_manufacturing/server/simulator.py @@ -0,0 +1,309 @@ +""" +Production Machine Simulator. + +Simulates a single production machine with realistic physics: +- Temperature dynamics (heat generation, dissipation) +- Vibration (based on wear and speed) +- Wear accumulation over time +- Failure probability based on conditions +- Production output +""" + +import random +from dataclasses import dataclass +from typing import Literal + + +# Constants +AMBIENT_TEMP = 20.0 # Celsius +MAX_TEMP = 100.0 # Celsius +CRITICAL_TEMP = 95.0 # Celsius - triggers critical alerts +WARNING_TEMP = 85.0 # Celsius - triggers warnings +MAX_SPEED = 100.0 # Maximum speed percentage +CRITICAL_VIBRATION = 0.8 # Critical vibration level +WARNING_VIBRATION = 0.6 # Warning vibration level +BASE_PRODUCTION_RATE = 100.0 # Units per hour at max speed +MAINTENANCE_DURATION = 2.0 # Hours + + +@dataclass +class SimulatedMachine: + """ + Simulates a single production machine with physical properties. + + Attributes: + machine_id: Unique identifier + status: Current operational status + temperature: Current temperature in Celsius + vibration: Vibration level (0-1) + speed: Current speed (0-100%) + wear_level: Cumulative wear (0-1, higher is worse) + health_score: Overall health (0-100) + last_maintenance_time: When maintenance last occurred + total_runtime: Total hours of operation + units_produced: Total units produced + defects_produced: Total defective units + """ + + machine_id: str = "M1" + status: Literal["running", "idle", "maintenance", "failed"] = "idle" + temperature: float = AMBIENT_TEMP + vibration: float = 0.1 + speed: float = 0.0 + wear_level: float = 0.0 + health_score: float = 100.0 + last_maintenance_time: float = 0.0 + total_runtime: float = 0.0 + units_produced: int = 0 + defects_produced: int = 0 + + def step(self, dt: float) -> dict: + """ + Advance simulation by dt hours. + + Args: + dt: Time step in hours + + Returns: + Dictionary with step results (units produced, defects, etc.) + """ + if self.status == "running": + # Update physical properties + self._update_temperature(dt) + self._update_vibration() + self._update_wear(dt) + self._update_health_score() + + # Check for failures + if self._check_failure(): + self.status = "failed" + return { + "units_produced": 0, + "defects": 0, + "failed": True, + "failure_reason": "Random failure based on conditions", + } + + # Produce output + units, defects = self._produce_output(dt) + self.units_produced += units + self.defects_produced += defects + self.total_runtime += dt + + return { + "units_produced": units, + "defects": defects, + "failed": False, + } + + elif self.status == "idle": + # Machine is idle - slowly cool down + self._cool_down(dt) + return {"units_produced": 0, "defects": 0, "failed": False} + + elif self.status == "maintenance": + # Machine under maintenance - can't produce + return {"units_produced": 0, "defects": 0, "failed": False} + + elif self.status == "failed": + # Machine failed - no production + return {"units_produced": 0, "defects": 0, "failed": True} + + return {"units_produced": 0, "defects": 0, "failed": False} + + def _update_temperature(self, dt: float): + """Update temperature based on operation.""" + # Heat generation proportional to speed + heat_generation = (self.speed / MAX_SPEED) * 10.0 # degrees/hour + + # Additional heat from poor conditions + if self.wear_level > 0.5: + heat_generation *= 1.0 + (self.wear_level - 0.5) + + # Heat dissipation (Newton's cooling) + heat_dissipation = (self.temperature - AMBIENT_TEMP) * 0.15 + + # Update temperature + self.temperature += (heat_generation - heat_dissipation) * dt + + # Clamp temperature + self.temperature = max(AMBIENT_TEMP, min(MAX_TEMP, self.temperature)) + + def _cool_down(self, dt: float): + """Cool down when idle.""" + cooling_rate = (self.temperature - AMBIENT_TEMP) * 0.3 + self.temperature -= cooling_rate * dt + self.temperature = max(AMBIENT_TEMP, self.temperature) + + def _update_vibration(self): + """Update vibration based on wear and speed.""" + base_vibration = 0.05 + + # Wear increases vibration + wear_factor = self.wear_level * 0.4 + + # Speed increases vibration + speed_factor = (self.speed / MAX_SPEED) * 0.2 + + # Temperature above optimal increases vibration + if self.temperature > 70: + temp_factor = (self.temperature - 70) / 30 * 0.2 + else: + temp_factor = 0.0 + + self.vibration = base_vibration + wear_factor + speed_factor + temp_factor + + # Add small random noise + self.vibration += random.uniform(-0.02, 0.02) + + # Clamp vibration + self.vibration = max(0.0, min(1.0, self.vibration)) + + def _update_wear(self, dt: float): + """Update wear level based on operating conditions.""" + if self.status != "running": + return + + # Base wear rate + wear_rate = 0.001 # per hour + + # High temperature increases wear + if self.temperature > WARNING_TEMP: + wear_rate *= 1.0 + ((self.temperature - WARNING_TEMP) / 15) * 0.5 + + # High speed increases wear + speed_factor = self.speed / MAX_SPEED + wear_rate *= 1.0 + (speed_factor * 0.3) + + # High vibration increases wear + if self.vibration > WARNING_VIBRATION: + wear_rate *= 1.0 + (self.vibration - WARNING_VIBRATION) * 0.5 + + # Update wear + self.wear_level += wear_rate * dt + + # Clamp wear + self.wear_level = min(1.0, self.wear_level) + + def _update_health_score(self): + """Calculate overall health score (0-100).""" + # Start at 100 + health = 100.0 + + # Wear reduces health + health -= self.wear_level * 50 + + # High temperature reduces health + if self.temperature > WARNING_TEMP: + health -= ((self.temperature - WARNING_TEMP) / 15) * 20 + + # High vibration reduces health + if self.vibration > WARNING_VIBRATION: + health -= ((self.vibration - WARNING_VIBRATION) / 0.4) * 15 + + # Clamp health + self.health_score = max(0.0, min(100.0, health)) + + def _check_failure(self) -> bool: + """Check if machine should fail based on conditions.""" + # Base failure probability (very low) + failure_prob = 0.00001 # per hour + + # Wear dramatically increases failure probability + if self.wear_level > 0.7: + failure_prob *= 1.0 + ((self.wear_level - 0.7) / 0.3) * 50 + + # Critical temperature increases failure probability + if self.temperature > CRITICAL_TEMP: + failure_prob *= 20 + + # Critical vibration increases failure probability + if self.vibration > CRITICAL_VIBRATION: + failure_prob *= 10 + + # Very low health dramatically increases failure + if self.health_score < 20: + failure_prob *= 15 + + return random.random() < failure_prob + + def _produce_output(self, dt: float) -> tuple[int, int]: + """ + Calculate production output and defects. + + Returns: + (units_produced, defects) + """ + # Base rate proportional to speed + rate = BASE_PRODUCTION_RATE * (self.speed / MAX_SPEED) + + # Poor conditions reduce output + if self.temperature > WARNING_TEMP: + rate *= 0.8 + if self.vibration > WARNING_VIBRATION: + rate *= 0.85 + if self.wear_level > 0.6: + rate *= 0.9 + + # Calculate units + units = int(rate * dt) + + # Calculate defects using defect_rate property + defects = int(units * self.defect_rate) + + return units, defects + + def set_speed(self, new_speed: float): + """Set machine speed (0-100).""" + self.speed = max(0.0, min(MAX_SPEED, new_speed)) + if self.speed > 0 and self.status == "idle": + self.status = "running" + elif self.speed == 0 and self.status == "running": + self.status = "idle" + + def perform_maintenance(self, simulation_time: float): + """Perform maintenance on the machine.""" + self.status = "maintenance" + self.wear_level = 0.0 + self.temperature = AMBIENT_TEMP + self.vibration = 0.05 + self.health_score = 100.0 + self.last_maintenance_time = simulation_time + + def complete_maintenance(self): + """Complete maintenance and return to idle.""" + self.status = "idle" + + def repair_failure(self, simulation_time: float): + """Repair a failed machine (emergency maintenance).""" + self.status = "maintenance" + self.wear_level = 0.1 # Not perfect after emergency repair + self.temperature = AMBIENT_TEMP + self.vibration = 0.1 + self.health_score = 90.0 + self.last_maintenance_time = simulation_time + + def get_hours_since_maintenance(self, current_time: float) -> float: + """Get hours since last maintenance.""" + return current_time - self.last_maintenance_time + + @property + def production_output(self) -> float: + """Get current production rate in units per hour.""" + if self.status != "running": + return 0.0 + # Production rate is proportional to speed + # Max production at 100% speed is 10 units/hour + return (self.speed / MAX_SPEED) * 10.0 + + @property + def defect_rate(self) -> float: + """Get current defect rate (0-1).""" + rate = 0.01 # Base 1% defect rate + if self.temperature > WARNING_TEMP: + rate += 0.02 + if self.vibration > WARNING_VIBRATION: + rate += 0.03 + if self.wear_level > 0.5: + rate += 0.02 + return min(rate, 1.0) diff --git a/src/envs/cognitive_manufacturing/tools/__init__.py b/src/envs/cognitive_manufacturing/tools/__init__.py new file mode 100644 index 00000000..afe85d0d --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/__init__.py @@ -0,0 +1,78 @@ +"""Manufacturing tools for cognitive manufacturing environment.""" + +from .base import ManufacturingTool, ToolResult +# Phase 0 tools +from .read_sensors import ReadSensorsTool +from .check_health import CheckHealthTool +from .adjust_speed import AdjustSpeedTool +from .schedule_maintenance import ScheduleMaintenanceTool +from .send_alert import SendAlertTool +# Phase 1 tools +from .get_line_status import GetLineStatusTool +from .transfer_material import TransferMaterialTool +from .optimize_line_speed import OptimizeLineSpeedTool +# Phase 2 tools +from .save_production_data import SaveProductionDataTool +from .query_production_history import QueryProductionHistoryTool +from .execute_sql import ExecuteSQLTool +from .export_to_csv import ExportToCSVTool +from .import_from_csv import ImportFromCSVTool +from .search_knowledge import SearchKnowledgeTool +from .add_knowledge import AddKnowledgeTool +# Phase 3 tools +from .predict_maintenance import PredictMaintenanceTool +from .detect_anomaly import DetectAnomalyTool +from .predict_quality import PredictQualityTool +from .optimize_with_rl import OptimizeWithRLTool +from .forecast_demand import ForecastDemandTool +# Phase 4 tools +from .inspect_product import InspectProductTool +from .record_defect import RecordDefectTool +from .update_qc_thresholds import UpdateQCThresholdsTool +from .check_inventory import CheckInventoryTool +from .order_materials import OrderMaterialsTool +from .update_stock_levels import UpdateStockLevelsTool +from .monitor_energy_usage import MonitorEnergyUsageTool +from .set_power_mode import SetPowerModeTool +from .simulate_scenario import SimulateScenarioTool +from .optimize_production_schedule import OptimizeProductionScheduleTool + +__all__ = [ + "ManufacturingTool", + "ToolResult", + # Phase 0 + "ReadSensorsTool", + "CheckHealthTool", + "AdjustSpeedTool", + "ScheduleMaintenanceTool", + "SendAlertTool", + # Phase 1 + "GetLineStatusTool", + "TransferMaterialTool", + "OptimizeLineSpeedTool", + # Phase 2 + "SaveProductionDataTool", + "QueryProductionHistoryTool", + "ExecuteSQLTool", + "ExportToCSVTool", + "ImportFromCSVTool", + "SearchKnowledgeTool", + "AddKnowledgeTool", + # Phase 3 + "PredictMaintenanceTool", + "DetectAnomalyTool", + "PredictQualityTool", + "OptimizeWithRLTool", + "ForecastDemandTool", + # Phase 4 + "InspectProductTool", + "RecordDefectTool", + "UpdateQCThresholdsTool", + "CheckInventoryTool", + "OrderMaterialsTool", + "UpdateStockLevelsTool", + "MonitorEnergyUsageTool", + "SetPowerModeTool", + "SimulateScenarioTool", + "OptimizeProductionScheduleTool", +] diff --git a/src/envs/cognitive_manufacturing/tools/add_knowledge.py b/src/envs/cognitive_manufacturing/tools/add_knowledge.py new file mode 100644 index 00000000..38ed7285 --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/add_knowledge.py @@ -0,0 +1,100 @@ +"""Tool for adding documents to knowledge base.""" + +from typing import Any, TYPE_CHECKING +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class AddKnowledgeTool(ManufacturingTool): + """Add document to knowledge base.""" + + @property + def name(self) -> str: + return "AddKnowledge" + + @property + def description(self) -> str: + return "Add a document to the knowledge base (maintenance guides, troubleshooting steps, safety procedures)" + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Document title", + }, + "content": { + "type": "string", + "description": "Document content (supports markdown)", + }, + "doc_type": { + "type": "string", + "enum": ["maintenance", "troubleshooting", "safety", "procedure", "general"], + "description": "Type of document", + "default": "general", + }, + "metadata": { + "type": "object", + "description": "Optional metadata (tags, machine_id, severity, etc.)", + }, + }, + "required": ["title", "content"], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + # Check if database and embedding service are enabled + if not hasattr(env, "db_manager") or env.db_manager is None: + return ToolResult( + success=False, + error="Database not enabled. Create environment with enable_database=True", + ) + + if not hasattr(env, "embedding_service") or env.embedding_service is None: + return ToolResult( + success=False, + error="Embedding service not enabled", + ) + + title = parameters["title"] + content = parameters["content"] + doc_type = parameters.get("doc_type", "general") + metadata = parameters.get("metadata") + + try: + # Generate embedding for the content + embedding = env.embedding_service.embed_text(content) + + # Add to knowledge base + doc_id = env.db_manager.add_knowledge( + title=title, + content=content, + doc_type=doc_type, + embedding=embedding, + metadata=metadata, + ) + + return ToolResult( + success=True, + data={ + "doc_id": doc_id, + "title": title, + "doc_type": doc_type, + "content_length": len(content), + "embedding_dimension": len(embedding), + }, + message=f"Added knowledge article '{title}' to knowledge base (ID: {doc_id})", + ) + + except Exception as e: + return ToolResult( + success=False, + error=f"Failed to add knowledge: {str(e)}", + ) diff --git a/src/envs/cognitive_manufacturing/tools/adjust_speed.py b/src/envs/cognitive_manufacturing/tools/adjust_speed.py new file mode 100644 index 00000000..db1987b9 --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/adjust_speed.py @@ -0,0 +1,88 @@ +"""Tool for adjusting machine speed.""" + +from typing import Any, TYPE_CHECKING +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class AdjustSpeedTool(ManufacturingTool): + """Adjust the operating speed of a machine.""" + + @property + def name(self) -> str: + return "AdjustSpeed" + + @property + def description(self) -> str: + return "Adjust machine operating speed (0-100%). Higher speeds increase throughput but may increase wear and temperature." + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "machine_id": {"type": "string", "description": "Machine identifier"}, + "target_speed": { + "type": "number", + "description": "Target speed percentage (0-100)", + "minimum": 0, + "maximum": 100, + }, + }, + "required": ["machine_id", "target_speed"], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + machine_id = parameters["machine_id"] + target_speed = parameters["target_speed"] + + # Get machine (support both single and multi-machine mode) + if hasattr(env, "production_line") and env.production_line is not None: + # Multi-machine mode: look up machine in production line + if machine_id not in env.production_line.machines: + return ToolResult(success=False, error=f"Machine '{machine_id}' not found") + machine = env.production_line.machines[machine_id] + else: + # Single machine mode + machine = env.simulator_machine + if machine.machine_id != machine_id: + return ToolResult(success=False, error=f"Machine '{machine_id}' not found") + + # Check if machine can have its speed adjusted + if machine.status == "failed": + return ToolResult( + success=False, + error=f"Cannot adjust speed: machine is in failed state. Maintenance required.", + ) + + if machine.status == "maintenance": + return ToolResult( + success=False, + error=f"Cannot adjust speed: machine is under maintenance", + ) + + old_speed = machine.speed + machine.set_speed(target_speed) + + # Update status based on new speed + if target_speed > 0: + machine.status = "running" + else: + machine.status = "idle" + + return ToolResult( + success=True, + data={ + "machine_id": machine_id, + "old_speed": round(old_speed, 1), + "new_speed": round(target_speed, 1), + "status": machine.status, + }, + message=f"Adjusted {machine_id} speed from {old_speed:.1f}% to {target_speed:.1f}%", + ) diff --git a/src/envs/cognitive_manufacturing/tools/base.py b/src/envs/cognitive_manufacturing/tools/base.py new file mode 100644 index 00000000..28c9c782 --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/base.py @@ -0,0 +1,118 @@ +""" +Base class for manufacturing tools. + +All tools inherit from ManufacturingTool and implement the execute method. +""" + +from abc import ABC, abstractmethod +from typing import Any, TYPE_CHECKING + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class ToolResult(dict): + """ + Standardized tool result. + + Always includes: + success: bool - Whether tool executed successfully + data: dict - Tool-specific result data + message: str - Human-readable message + error: str | None - Error message if failed + """ + + def __init__( + self, + success: bool, + data: dict[str, Any] | None = None, + message: str = "", + error: str | None = None, + ): + super().__init__( + success=success, + data=data or {}, + message=message, + error=error, + ) + + +class ManufacturingTool(ABC): + """Base class for all manufacturing tools.""" + + @property + @abstractmethod + def name(self) -> str: + """Tool name (must match action.tool_name).""" + pass + + @property + @abstractmethod + def description(self) -> str: + """Human-readable tool description.""" + pass + + @property + @abstractmethod + def parameters_schema(self) -> dict: + """ + JSON schema for tool parameters. + + Example: + { + "type": "object", + "properties": { + "machine_id": {"type": "string"}, + "threshold": {"type": "number", "minimum": 0} + }, + "required": ["machine_id"] + } + """ + pass + + @abstractmethod + def execute( + self, + parameters: dict[str, Any], + env: "CognitiveManufacturingEnvironment", + ) -> ToolResult: + """ + Execute the tool. + + Args: + parameters: Tool-specific parameters from agent + env: Reference to environment for accessing state, simulator, etc. + + Returns: + ToolResult with success status, data, and messages + """ + pass + + def validate_parameters(self, parameters: dict) -> tuple[bool, str | None]: + """ + Validate parameters against schema. + + Returns: + (is_valid, error_message) + """ + schema = self.parameters_schema + required = schema.get("required", []) + + # Check required parameters + for param in required: + if param not in parameters: + return False, f"Missing required parameter: {param}" + + # Basic type checking + properties = schema.get("properties", {}) + for param_name, value in parameters.items(): + if param_name in properties: + expected_type = properties[param_name].get("type") + if expected_type == "string" and not isinstance(value, str): + return False, f"Parameter '{param_name}' must be a string" + elif expected_type == "number" and not isinstance(value, (int, float)): + return False, f"Parameter '{param_name}' must be a number" + elif expected_type == "boolean" and not isinstance(value, bool): + return False, f"Parameter '{param_name}' must be a boolean" + + return True, None diff --git a/src/envs/cognitive_manufacturing/tools/check_health.py b/src/envs/cognitive_manufacturing/tools/check_health.py new file mode 100644 index 00000000..553058cf --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/check_health.py @@ -0,0 +1,83 @@ +"""Tool for checking machine health status.""" + +from typing import Any, TYPE_CHECKING +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class CheckHealthTool(ManufacturingTool): + """Check machine health and get diagnostic information.""" + + @property + def name(self) -> str: + return "CheckHealth" + + @property + def description(self) -> str: + return "Get machine health score and diagnostic information" + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": {"machine_id": {"type": "string", "description": "Machine identifier"}}, + "required": ["machine_id"], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + machine_id = parameters["machine_id"] + + # Get machine (support both single and multi-machine mode) + if hasattr(env, "production_line") and env.production_line is not None: + # Multi-machine mode: look up machine in production line + if machine_id not in env.production_line.machines: + return ToolResult(success=False, error=f"Machine '{machine_id}' not found") + machine = env.production_line.machines[machine_id] + else: + # Single machine mode + machine = env.simulator_machine + if machine.machine_id != machine_id: + return ToolResult(success=False, error=f"Machine '{machine_id}' not found") + + health_status = "FAILED" if machine.status == "failed" else ( + "GOOD" if machine.health_score >= 80 else + "FAIR" if machine.health_score >= 60 else + "POOR" if machine.health_score >= 40 else "CRITICAL" + ) + + diagnostics = { + "health_score": round(machine.health_score, 1), + "status": health_status, + "wear_level": round(machine.wear_level, 3), + "hours_since_maintenance": round(machine.get_hours_since_maintenance(env._state.simulation_time), 1), + "issues": [], + "recommendations": [], + } + + if machine.temperature > 85: + diagnostics["issues"].append(f"High temperature: {machine.temperature:.1f}C") + if machine.vibration > 0.6: + diagnostics["issues"].append(f"High vibration: {machine.vibration:.2f}") + if machine.wear_level > 0.6: + diagnostics["issues"].append(f"High wear: {machine.wear_level:.2%}") + if machine.status == "failed": + diagnostics["issues"].append("Machine has failed") + + if machine.wear_level > 0.7: + diagnostics["recommendations"].append("Schedule maintenance soon") + if machine.temperature > 90: + diagnostics["recommendations"].append("Reduce speed to cool down") + if machine.status == "failed": + diagnostics["recommendations"].append("Perform emergency repair") + + return ToolResult( + success=True, + data=diagnostics, + message=f"Health check for {machine_id}: {health_status} (score: {machine.health_score:.1f})", + ) diff --git a/src/envs/cognitive_manufacturing/tools/check_inventory.py b/src/envs/cognitive_manufacturing/tools/check_inventory.py new file mode 100644 index 00000000..3e5e2621 --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/check_inventory.py @@ -0,0 +1,129 @@ +"""Tool for checking material inventory levels.""" + +from typing import Any, TYPE_CHECKING +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class CheckInventoryTool(ManufacturingTool): + """Check material inventory levels and get reorder recommendations.""" + + @property + def name(self) -> str: + return "CheckInventory" + + @property + def description(self) -> str: + return "Check material inventory levels and get reorder recommendations" + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "material_type": { + "type": "string", + "enum": ["raw_material", "components", "finished_goods", "all"], + "description": "Type of materials to check", + "default": "all", + }, + "include_forecast": { + "type": "boolean", + "description": "Include demand forecast in recommendations", + "default": False, + }, + }, + "required": [], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + material_type = parameters.get("material_type", "all") + include_forecast = parameters.get("include_forecast", False) + + # Initialize inventory if not exists + if not hasattr(env, "_inventory"): + env._inventory = { + "steel_sheets": {"type": "raw_material", "level": 1000.0, "unit": "kg", "reorder_point": 200.0}, + "aluminum_bars": {"type": "raw_material", "level": 500.0, "unit": "kg", "reorder_point": 100.0}, + "fasteners": {"type": "components", "level": 5000.0, "unit": "units", "reorder_point": 1000.0}, + "circuit_boards": {"type": "components", "level": 200.0, "unit": "units", "reorder_point": 50.0}, + "finished_units": {"type": "finished_goods", "level": 0.0, "unit": "units", "reorder_point": 0.0}, + } + + # Filter by material type + inventory_levels = {} + for material_id, info in env._inventory.items(): + if material_type == "all" or info["type"] == material_type: + inventory_levels[material_id] = { + "current_level": info["level"], + "unit": info["unit"], + "reorder_point": info["reorder_point"], + "material_type": info["type"], + "status": "ok" if info["level"] > info["reorder_point"] else "low", + } + + # Find low stock items + low_stock_items = [] + for material_id, info in inventory_levels.items(): + if info["status"] == "low": + criticality = "critical" if info["current_level"] < info["reorder_point"] * 0.5 else "warning" + low_stock_items.append({ + "material_id": material_id, + "current_level": info["current_level"], + "reorder_point": info["reorder_point"], + "shortfall": info["reorder_point"] - info["current_level"], + "criticality": criticality, + }) + + # Generate reorder recommendations + reorder_recommendations = [] + for item in low_stock_items: + # Calculate recommended order quantity (2x reorder point) + recommended_qty = item["reorder_point"] * 2 + + # Adjust based on forecast if enabled + if include_forecast and hasattr(env, "ml_service") and env.ml_service: + try: + forecast = env.ml_service.forecast_demand(horizon=168) # 1 week + avg_demand = sum(f["demand"] for f in forecast) / len(forecast) + # Estimate material consumption (assuming 1 unit uses 10kg raw material) + weekly_consumption = avg_demand * 10 + recommended_qty = max(recommended_qty, weekly_consumption * 1.5) # 1.5 weeks buffer + except Exception: + pass # Use default if forecast fails + + reorder_recommendations.append({ + "material_id": item["material_id"], + "recommended_quantity": round(recommended_qty, 2), + "priority": "urgent" if item["criticality"] == "critical" else "normal", + "reason": f"Stock below reorder point ({item['current_level']:.0f} < {item['reorder_point']:.0f})", + }) + + # Calculate total inventory value (simplified) + total_items = sum(info["current_level"] for info in inventory_levels.values()) + storage_utilization = min(100.0, (total_items / 10000) * 100) # Assume 10k capacity + + message = f"Inventory check: {len(inventory_levels)} items tracked, {len(low_stock_items)} low stock" + + return ToolResult( + success=True, + data={ + "inventory_levels": inventory_levels, + "low_stock_items": low_stock_items, + "reorder_recommendations": reorder_recommendations, + "summary": { + "total_items_tracked": len(inventory_levels), + "low_stock_count": len(low_stock_items), + "reorder_needed": len(reorder_recommendations), + "storage_utilization_pct": round(storage_utilization, 1), + }, + "forecast_included": include_forecast, + }, + message=message, + ) diff --git a/src/envs/cognitive_manufacturing/tools/detect_anomaly.py b/src/envs/cognitive_manufacturing/tools/detect_anomaly.py new file mode 100644 index 00000000..156cded4 --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/detect_anomaly.py @@ -0,0 +1,123 @@ +"""Tool for detecting anomalies in sensor patterns using ML.""" + +from typing import Any, TYPE_CHECKING +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class DetectAnomalyTool(ManufacturingTool): + """Detect abnormal sensor patterns using ML.""" + + @property + def name(self) -> str: + return "DetectAnomaly" + + @property + def description(self) -> str: + return "Detect anomalies in sensor patterns for early warning of equipment issues" + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "machine_id": { + "type": "string", + "description": "Machine identifier", + }, + "window_size": { + "type": "integer", + "description": "Number of recent readings to analyze (default: 50)", + "minimum": 10, + "maximum": 500, + "default": 50, + }, + "sensitivity": { + "type": "number", + "description": "Detection sensitivity 0-1, higher=more sensitive (default: 0.5)", + "minimum": 0.0, + "maximum": 1.0, + "default": 0.5, + }, + }, + "required": ["machine_id"], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + if not hasattr(env, "ml_service") or env.ml_service is None: + return ToolResult( + success=False, + error="ML features not enabled. Create environment with enable_ml=True", + ) + + machine_id = parameters["machine_id"] + window_size = parameters.get("window_size", 50) + sensitivity = parameters.get("sensitivity", 0.5) + + # Get machine + if hasattr(env, "production_line") and env.production_line is not None: + if machine_id not in env.production_line.machines: + return ToolResult(success=False, error=f"Machine '{machine_id}' not found") + machine = env.production_line.machines[machine_id] + else: + machine = env.simulator_machine + if machine.machine_id != machine_id: + return ToolResult(success=False, error=f"Machine '{machine_id}' not found") + + # For demo, create synthetic recent history + sensor_readings = [] + for i in range(min(window_size, 50)): + sensor_readings.append({ + 'temperature': machine.temperature + (i * 0.1 - 2.5), + 'vibration': machine.vibration + (i * 0.01 - 0.25), + 'speed': machine.speed, + 'production_output': machine.production_output, + }) + + # Detect anomalies + result = env.ml_service.detect_anomalies(sensor_readings) + + # Generate explanation + explanation = {} + for sensor in result.get('anomalous_sensors', []): + if sensor == 'temperature': + explanation[sensor] = f"Temperature deviation detected: {machine.temperature:.1f}°C" + elif sensor == 'vibration': + explanation[sensor] = f"Vibration spike detected: {machine.vibration:.3f}" + elif sensor == 'speed': + explanation[sensor] = f"Unusual speed pattern: {machine.speed:.1f}%" + + # Recommended action + if result['anomaly_detected']: + if result['severity'] == 'high': + recommended_action = f"Immediately investigate {machine_id} - reduce speed and schedule maintenance" + elif result['severity'] == 'medium': + recommended_action = f"Monitor {machine_id} closely, prepare for maintenance" + else: + recommended_action = f"Continue monitoring {machine_id}" + else: + recommended_action = "No action needed, all sensors within normal range" + + return ToolResult( + success=True, + data={ + "machine_id": machine_id, + "anomaly_detected": result['anomaly_detected'], + "anomaly_score": round(result['anomaly_score'], 3), + "anomalous_sensors": result['anomalous_sensors'], + "explanation": explanation, + "severity": result['severity'], + "recommended_action": recommended_action, + "window_size": len(sensor_readings), + "sensitivity": sensitivity, + }, + message=f"Anomaly detection for {machine_id}: " + f"{'ANOMALY' if result['anomaly_detected'] else 'NORMAL'} " + f"(score: {result['anomaly_score']:.2f}, severity: {result['severity']})", + ) diff --git a/src/envs/cognitive_manufacturing/tools/execute_sql.py b/src/envs/cognitive_manufacturing/tools/execute_sql.py new file mode 100644 index 00000000..ed5fc703 --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/execute_sql.py @@ -0,0 +1,97 @@ +"""Tool for executing custom SQL queries.""" + +from typing import Any, TYPE_CHECKING +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class ExecuteSQLTool(ManufacturingTool): + """Execute custom SQL queries for advanced analytics.""" + + @property + def name(self) -> str: + return "ExecuteSQL" + + @property + def description(self) -> str: + return "Execute custom SQL SELECT queries for advanced analytics (read-only, safe execution)" + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "SQL SELECT query to execute (only SELECT allowed, no mutations)", + }, + "limit": { + "type": "integer", + "description": "Maximum number of rows to return (default: 1000)", + "minimum": 1, + "maximum": 10000, + "default": 1000, + }, + }, + "required": ["query"], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + # Check if database is enabled + if not hasattr(env, "db_manager") or env.db_manager is None: + return ToolResult( + success=False, + error="Database not enabled. Create environment with enable_database=True", + ) + + query = parameters["query"].strip() + limit = parameters.get("limit", 1000) + + # Add LIMIT clause if not present + query_upper = query.upper() + if "LIMIT" not in query_upper: + query = f"{query.rstrip(';')} LIMIT {limit}" + + try: + # Execute query (safety checks are in db_manager.execute_sql) + results = env.db_manager.execute_sql(query) + + # Format results + row_count = len(results) + + # Get column names + columns = list(results[0].keys()) if results else [] + + message = f"Query executed successfully. {row_count} rows returned" + if row_count >= limit: + message += f" (limited to {limit})" + + return ToolResult( + success=True, + data={ + "results": results, + "row_count": row_count, + "columns": columns, + "query": query, + }, + message=message, + ) + + except ValueError as e: + # Safety check failed + return ToolResult( + success=False, + error=f"Query validation failed: {str(e)}", + ) + except Exception as e: + # Query execution failed + return ToolResult( + success=False, + error=f"Query execution failed: {str(e)}", + ) diff --git a/src/envs/cognitive_manufacturing/tools/export_to_csv.py b/src/envs/cognitive_manufacturing/tools/export_to_csv.py new file mode 100644 index 00000000..0e7a4716 --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/export_to_csv.py @@ -0,0 +1,166 @@ +"""Tool for exporting data to CSV files.""" + +from typing import Any, TYPE_CHECKING +from datetime import datetime +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class ExportToCSVTool(ManufacturingTool): + """Export production data to CSV file.""" + + @property + def name(self) -> str: + return "ExportToCSV" + + @property + def description(self) -> str: + return "Export production data (runs, sensors, events, units) to CSV file for analysis" + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "data_type": { + "type": "string", + "enum": ["runs", "sensors", "events", "units"], + "description": "Type of data to export", + }, + "filename": { + "type": "string", + "description": "Output filename (with or without .csv extension)", + }, + "filters": { + "type": "object", + "description": "Optional filters (same as QueryProductionHistory)", + "properties": { + "start_date": {"type": "string"}, + "end_date": {"type": "string"}, + "min_reward": {"type": "number"}, + "status": {"type": "string"}, + "mode": {"type": "string"}, + "machine_id": {"type": "string"}, + }, + }, + }, + "required": ["data_type", "filename"], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + # Check if database and CSV service are enabled + if not hasattr(env, "db_manager") or env.db_manager is None: + return ToolResult( + success=False, + error="Database not enabled. Create environment with enable_database=True", + ) + + if not hasattr(env, "csv_service") or env.csv_service is None: + return ToolResult( + success=False, + error="CSV service not enabled", + ) + + data_type = parameters["data_type"] + filename = parameters["filename"] + filters = parameters.get("filters", {}) + + # Parse date filters if provided + if "start_date" in filters: + try: + filters["start_date"] = datetime.fromisoformat(filters["start_date"].replace("Z", "+00:00")) + except ValueError as e: + return ToolResult(success=False, error=f"Invalid start_date format: {e}") + + if "end_date" in filters: + try: + filters["end_date"] = datetime.fromisoformat(filters["end_date"].replace("Z", "+00:00")) + except ValueError as e: + return ToolResult(success=False, error=f"Invalid end_date format: {e}") + + try: + # Query data based on type + if data_type == "runs": + data = env.db_manager.query_runs(filters=filters, limit=10000) + + elif data_type == "sensors": + # Query sensor readings (need to add this method to db_manager) + # For now, use SQL query + query = "SELECT * FROM sensor_readings" + where_clauses = [] + + if "start_date" in filters: + where_clauses.append(f"timestamp >= '{filters['start_date'].isoformat()}'") + if "end_date" in filters: + where_clauses.append(f"timestamp <= '{filters['end_date'].isoformat()}'") + if "machine_id" in filters: + where_clauses.append(f"machine_id = '{filters['machine_id']}'") + + if where_clauses: + query += " WHERE " + " AND ".join(where_clauses) + + query += " ORDER BY timestamp DESC LIMIT 10000" + data = env.db_manager.execute_sql(query) + + elif data_type == "events": + # Query machine events + query = "SELECT * FROM machine_events" + where_clauses = [] + + if "start_date" in filters: + where_clauses.append(f"timestamp >= '{filters['start_date'].isoformat()}'") + if "end_date" in filters: + where_clauses.append(f"timestamp <= '{filters['end_date'].isoformat()}'") + if "machine_id" in filters: + where_clauses.append(f"machine_id = '{filters['machine_id']}'") + + if where_clauses: + query += " WHERE " + " AND ".join(where_clauses) + + query += " ORDER BY timestamp DESC LIMIT 10000" + data = env.db_manager.execute_sql(query) + + elif data_type == "units": + # Query production units + query = "SELECT * FROM production_units" + where_clauses = [] + + if "start_date" in filters: + where_clauses.append(f"created_at >= '{filters['start_date'].isoformat()}'") + if "end_date" in filters: + where_clauses.append(f"created_at <= '{filters['end_date'].isoformat()}'") + + if where_clauses: + query += " WHERE " + " AND ".join(where_clauses) + + query += " ORDER BY created_at DESC LIMIT 10000" + data = env.db_manager.execute_sql(query) + + else: + return ToolResult(success=False, error=f"Unknown data type: {data_type}") + + # Export to CSV + filepath = env.csv_service.export_to_csv(data, filename) + + return ToolResult( + success=True, + data={ + "filepath": filepath, + "data_type": data_type, + "row_count": len(data), + "filters_applied": filters, + }, + message=f"Exported {len(data)} {data_type} records to {filepath}", + ) + + except Exception as e: + return ToolResult( + success=False, + error=f"Export failed: {str(e)}", + ) diff --git a/src/envs/cognitive_manufacturing/tools/forecast_demand.py b/src/envs/cognitive_manufacturing/tools/forecast_demand.py new file mode 100644 index 00000000..b4f21e01 --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/forecast_demand.py @@ -0,0 +1,159 @@ +"""Tool for demand forecasting using time series analysis.""" + +from typing import Any, TYPE_CHECKING +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class ForecastDemandTool(ManufacturingTool): + """Forecast future production demand using time series forecasting.""" + + @property + def name(self) -> str: + return "ForecastDemand" + + @property + def description(self) -> str: + return "Forecast future production demand using time series analysis" + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "forecast_horizon": { + "type": "integer", + "description": "Number of hours ahead to forecast (default: 168 = 1 week)", + "default": 168, + }, + "confidence_level": { + "type": "number", + "description": "Confidence interval level (default: 0.95)", + "default": 0.95, + }, + "include_seasonality": { + "type": "boolean", + "description": "Account for seasonal patterns (default: true)", + "default": True, + }, + }, + "required": [], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + if not hasattr(env, "ml_service") or env.ml_service is None: + return ToolResult( + success=False, + error="ML features not enabled. Create environment with enable_ml=True", + ) + + forecast_horizon = parameters.get("forecast_horizon", 168) + confidence_level = parameters.get("confidence_level", 0.95) + include_seasonality = parameters.get("include_seasonality", True) + + # Get forecast from ML service + forecast = env.ml_service.forecast_demand(horizon=forecast_horizon) + + # Analyze trend + if len(forecast) >= 2: + early_avg = sum(f['demand'] for f in forecast[:len(forecast)//3]) / (len(forecast)//3) + late_avg = sum(f['demand'] for f in forecast[-len(forecast)//3:]) / (len(forecast)//3) + + if late_avg > early_avg * 1.1: + trend = "increasing" + elif late_avg < early_avg * 0.9: + trend = "decreasing" + else: + trend = "stable" + else: + trend = "stable" + + # Check for seasonality (simple check: variance in forecast) + import numpy as np + demands = [f['demand'] for f in forecast] + seasonality_detected = include_seasonality and (np.std(demands) / max(np.mean(demands), 1)) > 0.15 + + # Generate recommendations based on forecast + recommendations = [] + + # Check for capacity needs + avg_demand = sum(f['demand'] for f in forecast) / len(forecast) + peak_demand = max(f['demand'] for f in forecast) + + # Estimate current capacity + if hasattr(env, "production_line") and env.production_line is not None: + total_produced = sum(m.units_produced for m in env.production_line.machines.values()) + current_capacity = total_produced / max(env._state.step_count / 36000, 1) # Units per hour + else: + current_capacity = 100 # Default estimate + + if current_capacity > 0 and peak_demand > current_capacity * 0.9: + capacity_increase = ((peak_demand / current_capacity) - 1) * 100 + recommendations.append( + f"Increase production capacity by {capacity_increase:.0f}% to meet peak demand" + ) + + # Find low-demand periods for maintenance + low_demand_periods = [] + for i, f in enumerate(forecast): + if f['demand'] < avg_demand * 0.7: + low_demand_periods.append(i + 1) + + if low_demand_periods: + if len(low_demand_periods) >= 4: + # Find continuous periods + period_start = low_demand_periods[0] + period_end = low_demand_periods[0] + for h in low_demand_periods[1:]: + if h == period_end + 1: + period_end = h + else: + break + + if period_end - period_start >= 4: + recommendations.append( + f"Schedule maintenance during low-demand period (hours {period_start}-{period_end})" + ) + else: + recommendations.append( + f"Consider scheduling maintenance during low-demand hours: {low_demand_periods[:3]}" + ) + + # Warning for increasing trend + if trend == "increasing": + recommendations.append( + "Demand is trending upward. Consider preparing additional capacity." + ) + + # Calculate forecast accuracy if we have history + forecast_accuracy = 0.85 # Default + if len(env.ml_service.demand_history) >= 10: + # Simple accuracy estimate based on data variance + import numpy as np + historical_values = [d['volume'] for d in env.ml_service.demand_history] + cv = np.std(historical_values) / max(np.mean(historical_values), 1) # Coefficient of variation + forecast_accuracy = max(0.5, min(0.95, 1.0 - cv)) + + return ToolResult( + success=True, + data={ + "forecast": forecast, + "trend": trend, + "seasonality_detected": seasonality_detected, + "forecast_accuracy": round(forecast_accuracy, 2), + "recommendations": recommendations, + "summary": { + "avg_demand": round(avg_demand, 1), + "peak_demand": round(peak_demand, 1), + "min_demand": round(min(f['demand'] for f in forecast), 1), + "forecast_horizon_hours": forecast_horizon, + }, + }, + message=f"Forecast generated: {trend} trend, avg demand {avg_demand:.0f} units/hour", + ) diff --git a/src/envs/cognitive_manufacturing/tools/get_line_status.py b/src/envs/cognitive_manufacturing/tools/get_line_status.py new file mode 100644 index 00000000..0cc745c3 --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/get_line_status.py @@ -0,0 +1,99 @@ +"""Tool for getting production line status.""" + +from typing import Any, TYPE_CHECKING +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class GetLineStatusTool(ManufacturingTool): + """Get status of entire production line.""" + + @property + def name(self) -> str: + return "GetLineStatus" + + @property + def description(self) -> str: + return "Get comprehensive status of the entire production line including all machines, buffers, and line metrics" + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "include_buffers": { + "type": "boolean", + "description": "Include buffer level details (default: true)", + "default": True, + }, + "include_metrics": { + "type": "boolean", + "description": "Include line performance metrics (default: true)", + "default": True, + }, + }, + "required": [], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + include_buffers = parameters.get("include_buffers", True) + include_metrics = parameters.get("include_metrics", True) + + # Check if environment has production line simulator + if not hasattr(env, "production_line") or env.production_line is None: + # Fall back to single machine mode + machine = env.simulator_machine + return ToolResult( + success=True, + data={ + "mode": "single_machine", + "machine": { + "machine_id": machine.machine_id, + "status": machine.status, + "speed": machine.speed, + "temperature": machine.temperature, + "health_score": machine.health_score, + "production_output": machine.production_output, + "units_produced": machine.units_produced, + }, + }, + message="Single machine mode - no production line configured", + ) + + # Get full line status from production line simulator + line_status = env.production_line.get_line_status() + + # Build response based on what's requested + response_data = { + "mode": "production_line", + "machines": line_status["machines"], + } + + if include_buffers: + response_data["buffers"] = line_status["buffers"] + + if include_metrics: + response_data["metrics"] = line_status["metrics"] + + # Generate summary message + bottleneck = line_status["metrics"].get("bottleneck", "unknown") + throughput = line_status["metrics"].get("throughput_rate", 0.0) + efficiency = line_status["metrics"].get("line_efficiency", 0.0) + + message = ( + f"Production line status: {len(line_status['machines'])} machines, " + f"bottleneck={bottleneck}, throughput={throughput:.2f} units/hr, " + f"efficiency={efficiency:.1%}" + ) + + return ToolResult( + success=True, + data=response_data, + message=message, + ) diff --git a/src/envs/cognitive_manufacturing/tools/import_from_csv.py b/src/envs/cognitive_manufacturing/tools/import_from_csv.py new file mode 100644 index 00000000..453a1817 --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/import_from_csv.py @@ -0,0 +1,148 @@ +"""Tool for importing data from CSV files.""" + +from typing import Any, TYPE_CHECKING +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class ImportFromCSVTool(ManufacturingTool): + """Import data from CSV file.""" + + @property + def name(self) -> str: + return "ImportFromCSV" + + @property + def description(self) -> str: + return "Import data from CSV file (knowledge base articles, configurations, etc.)" + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "filename": { + "type": "string", + "description": "Input CSV filename (with or without .csv extension)", + }, + "data_type": { + "type": "string", + "enum": ["knowledge", "settings", "configurations"], + "description": "Type of data being imported", + }, + "validate": { + "type": "boolean", + "description": "Validate data before import (default: true)", + "default": True, + }, + }, + "required": ["filename", "data_type"], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + # Check if CSV service is enabled + if not hasattr(env, "csv_service") or env.csv_service is None: + return ToolResult( + success=False, + error="CSV service not enabled", + ) + + filename = parameters["filename"] + data_type = parameters["data_type"] + validate = parameters.get("validate", True) + + try: + # Validate if requested + if validate: + if data_type == "knowledge": + required_columns = ["title", "content", "doc_type"] + elif data_type == "settings": + required_columns = ["key", "value"] + elif data_type == "configurations": + required_columns = ["machine_id", "parameter", "value"] + else: + return ToolResult(success=False, error=f"Unknown data type: {data_type}") + + is_valid_csv, validation_error = env.csv_service.validate_csv(filename, required_columns) + if not is_valid_csv: + return ToolResult( + success=False, + error=f"CSV validation failed: {validation_error}", + ) + + # Import data + data = env.csv_service.import_from_csv(filename) + + # Process based on data type + imported_count = 0 + + if data_type == "knowledge": + # Import to knowledge base + if not hasattr(env, "db_manager") or env.db_manager is None: + return ToolResult( + success=False, + error="Database not enabled. Create environment with enable_database=True", + ) + + if not hasattr(env, "embedding_service") or env.embedding_service is None: + return ToolResult( + success=False, + error="Embedding service not enabled", + ) + + # Add each knowledge article + for row in data: + title = row.get("title", "") + content = row.get("content", "") + doc_type = row.get("doc_type", "general") + metadata = {k: v for k, v in row.items() if k not in ["title", "content", "doc_type"]} + + # Generate embedding + embedding = env.embedding_service.embed_text(content) + + # Add to database + env.db_manager.add_knowledge( + title=title, + content=content, + doc_type=doc_type, + embedding=embedding, + metadata=metadata if metadata else None, + ) + imported_count += 1 + + elif data_type == "settings": + # Import settings (store in environment or database) + # For now, just validate and return + imported_count = len(data) + + elif data_type == "configurations": + # Import machine configurations + imported_count = len(data) + + return ToolResult( + success=True, + data={ + "filename": filename, + "data_type": data_type, + "imported_count": imported_count, + "total_rows": len(data), + }, + message=f"Imported {imported_count} {data_type} records from {filename}", + ) + + except FileNotFoundError as e: + return ToolResult( + success=False, + error=f"File not found: {str(e)}", + ) + except Exception as e: + return ToolResult( + success=False, + error=f"Import failed: {str(e)}", + ) diff --git a/src/envs/cognitive_manufacturing/tools/inspect_product.py b/src/envs/cognitive_manufacturing/tools/inspect_product.py new file mode 100644 index 00000000..dd14c489 --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/inspect_product.py @@ -0,0 +1,181 @@ +"""Tool for detailed product quality inspection.""" + +from typing import Any, TYPE_CHECKING +import random +from datetime import datetime +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class InspectProductTool(ManufacturingTool): + """Perform detailed quality inspection on a production unit.""" + + @property + def name(self) -> str: + return "InspectProduct" + + @property + def description(self) -> str: + return "Perform detailed quality inspection on a production unit" + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "unit_id": { + "type": "string", + "description": "ID of the unit to inspect", + }, + "inspection_type": { + "type": "string", + "enum": ["visual", "dimensional", "functional", "full"], + "description": "Type of inspection to perform", + "default": "full", + }, + "auto_log_defects": { + "type": "boolean", + "description": "Automatically log defects found (default: true)", + "default": True, + }, + }, + "required": ["unit_id"], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + unit_id = parameters["unit_id"] + inspection_type = parameters.get("inspection_type", "full") + auto_log_defects = parameters.get("auto_log_defects", True) + + # Get machine state to determine quality factors + if hasattr(env, "production_line") and env.production_line is not None: + # Multi-machine: use average conditions + machines = list(env.production_line.machines.values()) + avg_temp = sum(m.temperature for m in machines) / len(machines) + avg_vibration = sum(m.vibration for m in machines) / len(machines) + avg_wear = sum(m.wear_level for m in machines) / len(machines) + avg_health = sum(m.health_score for m in machines) / len(machines) + else: + # Single machine + machine = env.simulator_machine + avg_temp = machine.temperature + avg_vibration = machine.vibration + avg_wear = machine.wear_level + avg_health = machine.health_score + + # Simulate inspection based on machine conditions + defects_found = [] + quality_score = 100.0 + + # Visual inspection + if inspection_type in ["visual", "full"]: + if avg_temp > 80: + defects_found.append({ + "type": "surface_discoloration", + "severity": "minor", + "description": f"Temperature-related surface marks ({avg_temp:.1f}°C)", + }) + quality_score -= 5 + + if avg_wear > 60: + defects_found.append({ + "type": "surface_roughness", + "severity": "minor", + "description": f"Tooling wear marks (wear level: {avg_wear:.0f}%)", + }) + quality_score -= 3 + + # Dimensional inspection + if inspection_type in ["dimensional", "full"]: + if avg_vibration > 8: + defects_found.append({ + "type": "dimensional_variance", + "severity": "major", + "description": f"High vibration caused tolerance issues ({avg_vibration:.1f} mm/s)", + }) + quality_score -= 15 + + if avg_temp > 85 or avg_temp < 50: + defects_found.append({ + "type": "thermal_expansion", + "severity": "minor", + "description": f"Temperature outside optimal range ({avg_temp:.1f}°C)", + }) + quality_score -= 5 + + # Functional inspection + if inspection_type in ["functional", "full"]: + if avg_health < 70: + defects_found.append({ + "type": "functional_failure", + "severity": "critical", + "description": f"Machine health degradation affected functionality ({avg_health:.0f}%)", + }) + quality_score -= 25 + + # Random functional test (simulated) + if random.random() > (avg_health / 100.0): + defects_found.append({ + "type": "performance_degradation", + "severity": "major", + "description": "Unit failed performance test", + }) + quality_score -= 20 + + # Ensure quality score doesn't go negative + quality_score = max(0.0, quality_score) + + # Determine pass/fail + qc_threshold = 70.0 # Can be configured via UpdateQCThresholds + passed = quality_score >= qc_threshold and all(d["severity"] != "critical" for d in defects_found) + + # Auto-log defects if enabled and database available + defect_ids = [] + if auto_log_defects and defects_found and hasattr(env, "db_manager") and env.db_manager: + for defect in defects_found: + # Use RecordDefect functionality + defect_id = f"DEF-{unit_id}-{len(defect_ids)+1}" + defect_ids.append(defect_id) + defect["defect_id"] = defect_id + + # Inspection details + inspection_details = { + "unit_id": unit_id, + "inspection_type": inspection_type, + "inspected_at": datetime.utcnow().isoformat(), + "inspector": "automated_qc", + "test_results": { + "visual": inspection_type in ["visual", "full"], + "dimensional": inspection_type in ["dimensional", "full"], + "functional": inspection_type in ["functional", "full"], + }, + "conditions": { + "temperature": round(avg_temp, 1), + "vibration": round(avg_vibration, 2), + "wear_level": round(avg_wear, 1), + "health_score": round(avg_health, 1), + }, + } + + message = f"Inspection {'PASSED' if passed else 'FAILED'}: {unit_id} - Quality score: {quality_score:.0f}%" + if defects_found: + message += f", {len(defects_found)} defects found" + + return ToolResult( + success=True, + data={ + "passed": passed, + "quality_score": round(quality_score, 1), + "defects_found": defects_found, + "defect_count": len(defects_found), + "inspection_details": inspection_details, + "defect_ids": defect_ids if auto_log_defects else [], + }, + message=message, + ) diff --git a/src/envs/cognitive_manufacturing/tools/monitor_energy_usage.py b/src/envs/cognitive_manufacturing/tools/monitor_energy_usage.py new file mode 100644 index 00000000..0783b4fe --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/monitor_energy_usage.py @@ -0,0 +1,164 @@ +"""Tool for monitoring energy usage and costs.""" + +from typing import Any, TYPE_CHECKING +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class MonitorEnergyUsageTool(ManufacturingTool): + """Monitor power consumption and energy costs.""" + + @property + def name(self) -> str: + return "MonitorEnergyUsage" + + @property + def description(self) -> str: + return "Monitor power consumption, energy usage, and costs" + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "time_window": { + "type": "integer", + "description": "Time window in hours to analyze (default: 24)", + "default": 24, + }, + "breakdown_by": { + "type": "string", + "enum": ["machine", "operation", "total"], + "description": "How to break down energy usage", + "default": "machine", + }, + "include_cost": { + "type": "boolean", + "description": "Include cost estimates", + "default": True, + }, + }, + "required": [], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + time_window = parameters.get("time_window", 24) + breakdown_by = parameters.get("breakdown_by", "machine") + include_cost = parameters.get("include_cost", True) + + # Energy pricing ($/kWh) + electricity_rate = 0.12 # $0.12 per kWh + + # Get machine states + if hasattr(env, "production_line") and env.production_line is not None: + machines = env.production_line.machines + else: + machines = {env.machine_id: env.simulator_machine} + + # Calculate energy usage per machine + breakdown = {} + total_kwh = 0.0 + + for machine_id, machine in machines.items(): + # Energy model based on speed and operating state + # Idle: 5kW, Running: 10-50kW depending on speed + if machine.speed == 0: + power_kw = 5.0 # Idle power + else: + # Power scales with speed (10kW at 0% to 50kW at 100%) + power_kw = 10.0 + (machine.speed / 100.0) * 40.0 + + # Calculate energy for the time window + kwh = power_kw * time_window + + # Apply power mode multiplier if set + power_mode = getattr(machine, "_power_mode", "normal") + if power_mode == "eco": + kwh *= 0.8 # 20% savings + elif power_mode == "high_performance": + kwh *= 1.2 # 20% more consumption + + total_kwh += kwh + + breakdown[machine_id] = { + "current_power_kw": round(power_kw, 2), + "energy_kwh": round(kwh, 2), + "power_mode": power_mode, + "operating_hours": time_window, + "average_speed": round(machine.speed, 1), + } + + if include_cost: + breakdown[machine_id]["cost_usd"] = round(kwh * electricity_rate, 2) + + # Calculate total cost + total_cost = total_kwh * electricity_rate if include_cost else 0.0 + + # Calculate efficiency score (lower energy per unit produced = better) + if hasattr(env, "production_line"): + total_units = sum(m.units_produced for m in machines.values()) + else: + total_units = env.simulator_machine.units_produced + + if total_units > 0: + energy_per_unit = total_kwh / total_units + # Efficiency score: 100 = excellent (< 1 kWh/unit), 0 = poor (> 10 kWh/unit) + efficiency_score = max(0.0, min(100.0, 100.0 * (1.0 - min(energy_per_unit / 10.0, 1.0)))) + else: + energy_per_unit = 0.0 + efficiency_score = 50.0 # Neutral score if no production + + # Generate recommendations + recommendations = [] + + # Check for high power mode usage + high_power_machines = [m_id for m_id, m in breakdown.items() if m.get("power_mode") == "high_performance"] + if high_power_machines: + recommendations.append(f"Consider switching {', '.join(high_power_machines)} to normal mode to save energy") + + # Check for idle machines with high power + idle_machines = [m_id for m_id, info in breakdown.items() + if info["average_speed"] == 0 and info["current_power_kw"] > 3] + if idle_machines: + recommendations.append(f"Idle machines consuming power: {', '.join(idle_machines)} - consider eco mode") + + # Check efficiency + if efficiency_score < 50: + recommendations.append("Low energy efficiency - review production parameters and maintenance needs") + elif efficiency_score > 80: + recommendations.append("Excellent energy efficiency - current settings are optimal") + + # Peak demand warning + total_power_kw = sum(info["current_power_kw"] for info in breakdown.values()) + if total_power_kw > 150: + recommendations.append(f"High power demand ({total_power_kw:.0f} kW) - consider load balancing") + + message = f"Energy usage: {total_kwh:.1f} kWh over {time_window}h" + if include_cost: + message += f" (${total_cost:.2f})" + + return ToolResult( + success=True, + data={ + "total_kwh": round(total_kwh, 2), + "cost": round(total_cost, 2) if include_cost else None, + "breakdown": breakdown if breakdown_by in ["machine", "total"] else {}, + "efficiency_score": round(efficiency_score, 1), + "energy_per_unit": round(energy_per_unit, 3), + "recommendations": recommendations, + "summary": { + "time_window_hours": time_window, + "total_power_kw": round(total_power_kw, 2), + "average_power_kw": round(total_kwh / time_window, 2), + "units_produced": total_units, + "electricity_rate": electricity_rate, + }, + }, + message=message, + ) diff --git a/src/envs/cognitive_manufacturing/tools/optimize_line_speed.py b/src/envs/cognitive_manufacturing/tools/optimize_line_speed.py new file mode 100644 index 00000000..6f1d0309 --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/optimize_line_speed.py @@ -0,0 +1,148 @@ +"""Tool for optimizing production line speed.""" + +from typing import Any, TYPE_CHECKING +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class OptimizeLineSpeedTool(ManufacturingTool): + """Automatically balance all machine speeds for optimal throughput.""" + + @property + def name(self) -> str: + return "OptimizeLineSpeed" + + @property + def description(self) -> str: + return "Automatically optimize all machine speeds to maximize throughput, quality, energy efficiency, or balance" + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "optimization_target": { + "type": "string", + "enum": ["throughput", "quality", "energy", "balanced"], + "description": "Optimization objective: maximize throughput, maximize quality, minimize energy, or balance all factors", + "default": "balanced", + }, + "constraints": { + "type": "object", + "description": "Optional speed constraints per machine (e.g., {'M1': {'min': 30, 'max': 80}})", + "properties": { + "M1": { + "type": "object", + "properties": { + "min": {"type": "number", "minimum": 0, "maximum": 100}, + "max": {"type": "number", "minimum": 0, "maximum": 100}, + }, + }, + "M2": { + "type": "object", + "properties": { + "min": {"type": "number", "minimum": 0, "maximum": 100}, + "max": {"type": "number", "minimum": 0, "maximum": 100}, + }, + }, + "M3": { + "type": "object", + "properties": { + "min": {"type": "number", "minimum": 0, "maximum": 100}, + "max": {"type": "number", "minimum": 0, "maximum": 100}, + }, + }, + "M4": { + "type": "object", + "properties": { + "min": {"type": "number", "minimum": 0, "maximum": 100}, + "max": {"type": "number", "minimum": 0, "maximum": 100}, + }, + }, + }, + }, + }, + "required": [], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + optimization_target = parameters.get("optimization_target", "balanced") + constraints = parameters.get("constraints", {}) + + # Check if environment has production line simulator + if not hasattr(env, "production_line") or env.production_line is None: + return ToolResult( + success=False, + error="OptimizeLineSpeed only works in production line mode (multi-machine)", + ) + + production_line = env.production_line + + # Get optimization results from production line + optimization_result = production_line.optimize_line_speed(target=optimization_target) + + # Apply constraints if provided + if constraints: + for machine_id, machine_constraints in constraints.items(): + if machine_id not in production_line.machines: + continue + + machine = production_line.machines[machine_id] + current_speed = machine.speed + + min_speed = machine_constraints.get("min", 0.0) + max_speed = machine_constraints.get("max", 100.0) + + # Clamp speed to constraints + if current_speed < min_speed: + machine.set_speed(min_speed) + optimization_result["new_speeds"][machine_id] = min_speed + elif current_speed > max_speed: + machine.set_speed(max_speed) + optimization_result["new_speeds"][machine_id] = max_speed + + # Calculate speed changes + speed_changes = {} + for machine_id in optimization_result["old_speeds"]: + old = optimization_result["old_speeds"][machine_id] + new = optimization_result["new_speeds"][machine_id] + change = new - old + speed_changes[machine_id] = { + "old": old, + "new": new, + "change": change, + "change_pct": (change / old * 100) if old > 0 else 0.0, + } + + # Build result + result_data = { + "optimization_target": optimization_target, + "speed_changes": speed_changes, + "bottleneck": optimization_result["bottleneck"], + "expected_throughput": optimization_result["expected_throughput"], + "expected_energy": optimization_result["expected_energy"], + } + + if constraints: + result_data["constraints_applied"] = constraints + + # Generate summary message + avg_change = sum(abs(sc["change"]) for sc in speed_changes.values()) / len(speed_changes) + message = ( + f"Optimized line for '{optimization_target}': " + f"bottleneck={optimization_result['bottleneck']}, " + f"expected throughput={optimization_result['expected_throughput']:.2f} units/hr, " + f"avg speed change={avg_change:.1f}%" + ) + + return ToolResult( + success=True, + data=result_data, + message=message, + ) diff --git a/src/envs/cognitive_manufacturing/tools/optimize_production_schedule.py b/src/envs/cognitive_manufacturing/tools/optimize_production_schedule.py new file mode 100644 index 00000000..515297ac --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/optimize_production_schedule.py @@ -0,0 +1,219 @@ +"""Tool for optimizing production schedules.""" + +from typing import Any, TYPE_CHECKING +from datetime import datetime, timedelta +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class OptimizeProductionScheduleTool(ManufacturingTool): + """Optimize production schedule for efficiency.""" + + @property + def name(self) -> str: + return "OptimizeProductionSchedule" + + @property + def description(self) -> str: + return "Optimize production schedule to maximize efficiency and minimize costs" + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "optimization_goal": { + "type": "string", + "enum": ["throughput", "cost", "quality", "energy", "balanced"], + "description": "Primary optimization objective", + "default": "balanced", + }, + "time_horizon": { + "type": "integer", + "description": "Planning horizon in hours (default: 168 = 1 week)", + "default": 168, + }, + "constraints": { + "type": "object", + "description": "Constraints (min_quality, max_energy, etc.)", + "default": {}, + }, + "include_maintenance": { + "type": "boolean", + "description": "Include scheduled maintenance in optimization", + "default": True, + }, + }, + "required": [], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + optimization_goal = parameters.get("optimization_goal", "balanced") + time_horizon = parameters.get("time_horizon", 168) + constraints = parameters.get("constraints", {}) + include_maintenance = parameters.get("include_maintenance", True) + + # Get current machine states + if hasattr(env, "production_line") and env.production_line is not None: + machines = list(env.production_line.machines.keys()) + current_health = {m_id: m.health_score for m_id, m in env.production_line.machines.items()} + else: + machines = [env.machine_id] + current_health = {env.machine_id: env.simulator_machine.health_score} + + # Get demand forecast if available + forecast_demand = [] + if hasattr(env, "ml_service") and env.ml_service: + try: + forecast = env.ml_service.forecast_demand(horizon=time_horizon) + forecast_demand = [f["demand"] for f in forecast] + except: + # Use default flat demand + avg_demand = 100 + forecast_demand = [avg_demand] * time_horizon + + if not forecast_demand: + forecast_demand = [100] * time_horizon # Default demand + + # Generate optimized schedule + schedule = [] + current_time = datetime.utcnow() + + # Optimization strategy based on goal + if optimization_goal == "throughput": + base_speed = 90 # High speed + base_power_mode = "high_performance" + elif optimization_goal == "energy": + base_speed = 70 # Moderate speed + base_power_mode = "eco" + elif optimization_goal == "quality": + base_speed = 60 # Slower for quality + base_power_mode = "normal" + elif optimization_goal == "cost": + base_speed = 75 # Balanced + base_power_mode = "eco" + else: # balanced + base_speed = 75 + base_power_mode = "normal" + + # Schedule maintenance windows for machines needing it + maintenance_schedule = [] + if include_maintenance: + for m_id, health in current_health.items(): + if health < 70: # Schedule maintenance for unhealthy machines + # Find low-demand period + min_demand_hour = forecast_demand.index(min(forecast_demand)) + maintenance_time = current_time + timedelta(hours=min_demand_hour) + maintenance_schedule.append({ + "machine_id": m_id, + "scheduled_time": maintenance_time.isoformat(), + "duration_hours": 4, + "reason": f"Preventive maintenance (health: {health:.0f}%)", + }) + + # Generate hourly schedule + for hour in range(min(24, time_horizon)): # First 24 hours detailed schedule + schedule_time = current_time + timedelta(hours=hour) + demand = forecast_demand[hour] if hour < len(forecast_demand) else 100 + + # Adjust speed based on demand + if demand > 120: + speed = min(100, base_speed + 10) + power_mode = "high_performance" + elif demand < 80: + speed = max(50, base_speed - 10) + power_mode = "eco" + else: + speed = base_speed + power_mode = base_power_mode + + # Check if maintenance scheduled for this hour + maint_this_hour = [m for m in maintenance_schedule + if abs((datetime.fromisoformat(m["scheduled_time"]) - schedule_time).total_seconds()) < 3600] + + schedule.append({ + "hour": hour, + "time": schedule_time.isoformat(), + "recommended_speed": speed, + "power_mode": power_mode, + "forecasted_demand": round(demand, 1), + "maintenance_planned": len(maint_this_hour) > 0, + "machines_in_maintenance": [m["machine_id"] for m in maint_this_hour], + }) + + # Calculate expected improvement + baseline_throughput = sum(forecast_demand[:24]) * 0.75 # 75% efficiency baseline + optimized_throughput = sum(s["recommended_speed"] / 100.0 * s["forecasted_demand"] + for s in schedule if not s["maintenance_planned"]) + + improvement_pct = ((optimized_throughput - baseline_throughput) / baseline_throughput * 100) if baseline_throughput > 0 else 0 + + # Generate recommended actions + recommended_actions = [] + + # Speed adjustments + if schedule[0]["recommended_speed"] != 75: + recommended_actions.append({ + "tool_name": "AdjustSpeed", + "parameters": {"machine_id": "all", "new_speed": schedule[0]["recommended_speed"]}, + "reason": f"Optimize for {optimization_goal}", + "priority": "high", + }) + + # Power mode + if schedule[0]["power_mode"] != "normal": + recommended_actions.append({ + "tool_name": "SetPowerMode", + "parameters": {"machine_id": "all", "power_mode": schedule[0]["power_mode"]}, + "reason": "Energy optimization", + "priority": "medium", + }) + + # Maintenance + for maint in maintenance_schedule: + recommended_actions.append({ + "tool_name": "ScheduleMaintenance", + "parameters": { + "machine_id": maint["machine_id"], + "schedule_for": maint["scheduled_time"], + }, + "reason": maint["reason"], + "priority": "high" if current_health[maint["machine_id"]] < 50 else "medium", + }) + + # Schedule details summary + schedule_summary = { + "total_hours_planned": time_horizon, + "detailed_hours": len(schedule), + "maintenance_windows": len(maintenance_schedule), + "avg_recommended_speed": round(sum(s["recommended_speed"] for s in schedule) / len(schedule), 1), + "total_forecasted_demand": round(sum(s["forecasted_demand"] for s in schedule), 1), + } + + message = f"Production schedule optimized for '{optimization_goal}' over {time_horizon}h" + + return ToolResult( + success=True, + data={ + "optimized_schedule": schedule, + "expected_improvement_pct": round(improvement_pct, 1), + "schedule_details": schedule_summary, + "maintenance_schedule": maintenance_schedule, + "recommended_actions": recommended_actions, + "optimization_summary": { + "goal": optimization_goal, + "time_horizon_hours": time_horizon, + "constraints_applied": constraints, + "machines_optimized": machines, + "expected_throughput": round(optimized_throughput, 1), + "baseline_throughput": round(baseline_throughput, 1), + }, + }, + message=message, + ) diff --git a/src/envs/cognitive_manufacturing/tools/optimize_with_rl.py b/src/envs/cognitive_manufacturing/tools/optimize_with_rl.py new file mode 100644 index 00000000..61f7854c --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/optimize_with_rl.py @@ -0,0 +1,146 @@ +"""Tool for RL-based production optimization.""" + +from typing import Any, TYPE_CHECKING +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class OptimizeWithRLTool(ManufacturingTool): + """Use reinforcement learning for intelligent production optimization.""" + + @property + def name(self) -> str: + return "OptimizeWithRL" + + @property + def description(self) -> str: + return "Use RL agent to suggest optimal actions for production optimization" + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "optimization_goal": { + "type": "string", + "enum": ["throughput", "quality", "cost", "energy", "balanced"], + "description": "Optimization objective", + "default": "balanced", + }, + "constraints": { + "type": "object", + "description": "Optional constraints (speed limits, etc.)", + }, + "learning_mode": { + "type": "string", + "enum": ["exploit", "explore"], + "description": "Exploit learned policy or explore new actions", + "default": "exploit", + }, + }, + "required": [], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + if not hasattr(env, "ml_service") or env.ml_service is None: + return ToolResult( + success=False, + error="ML features not enabled. Create environment with enable_ml=True", + ) + + optimization_goal = parameters.get("optimization_goal", "balanced") + constraints = parameters.get("constraints", {}) + learning_mode = parameters.get("learning_mode", "exploit") + + # Get current state + if hasattr(env, "production_line") and env.production_line is not None: + # Multi-machine state + machines = env.production_line.machines + avg_health = sum(m.health_score for m in machines.values()) / len(machines) + avg_temp = sum(m.temperature for m in machines.values()) / len(machines) + avg_speed = sum(m.speed for m in machines.values()) / len(machines) + else: + # Single machine state + machine = env.simulator_machine + avg_health = machine.health_score + avg_temp = machine.temperature + avg_speed = machine.speed + + state = { + 'health_score': avg_health, + 'temperature': avg_temp, + 'speed': avg_speed, + } + + # Define available actions based on current state + available_actions = [] + + # Speed adjustment actions + if avg_speed < 80: + available_actions.append({ + 'tool_name': 'AdjustSpeed', + 'parameters': {'machine_id': 'M1', 'target_speed': min(avg_speed + 10, 85)}, + 'description': 'Increase speed' + }) + if avg_speed > 40: + available_actions.append({ + 'tool_name': 'AdjustSpeed', + 'parameters': {'machine_id': 'M1', 'target_speed': max(avg_speed - 10, 35)}, + 'description': 'Decrease speed' + }) + + # Maintenance action + if avg_health < 80: + available_actions.append({ + 'tool_name': 'ScheduleMaintenance', + 'parameters': {'machine_id': 'M1', 'maintenance_type': 'scheduled'}, + 'description': 'Schedule maintenance' + }) + + if not available_actions: + available_actions.append({ + 'tool_name': 'ReadSensors', + 'parameters': {'machine_id': 'M1', 'sensors': 'all'}, + 'description': 'Monitor sensors' + }) + + # Get RL recommendation + rl_result = env.ml_service.select_action_rl( + state=state, + available_actions=available_actions, + learning_mode=learning_mode, + ) + + # Format recommended actions + recommended_actions = [{ + "tool_name": rl_result['action']['tool_name'], + "parameters": rl_result['action']['parameters'], + "expected_reward": round(rl_result['expected_reward'], 2), + "confidence": round(rl_result['confidence'], 2), + "description": rl_result['action'].get('description', 'Recommended action'), + }] + + return ToolResult( + success=True, + data={ + "optimization_goal": optimization_goal, + "recommended_actions": recommended_actions, + "policy_type": "q_learning", + "exploration_rate": env.ml_service.exploration_rate if learning_mode == "explore" else 0.0, + "learning_mode": learning_mode, + "current_state": { + "avg_health": round(avg_health, 1), + "avg_temperature": round(avg_temp, 1), + "avg_speed": round(avg_speed, 1), + }, + "available_actions_count": len(available_actions), + }, + message=f"RL recommendation ({learning_mode} mode): {recommended_actions[0]['tool_name']} " + f"(expected reward: {recommended_actions[0]['expected_reward']:.2f})", + ) diff --git a/src/envs/cognitive_manufacturing/tools/order_materials.py b/src/envs/cognitive_manufacturing/tools/order_materials.py new file mode 100644 index 00000000..97fa4628 --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/order_materials.py @@ -0,0 +1,161 @@ +"""Tool for ordering raw materials.""" + +from typing import Any, TYPE_CHECKING +import uuid +from datetime import datetime, timedelta +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class OrderMaterialsTool(ManufacturingTool): + """Order raw materials from suppliers.""" + + @property + def name(self) -> str: + return "OrderMaterials" + + @property + def description(self) -> str: + return "Order raw materials from suppliers" + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "material_id": { + "type": "string", + "description": "ID of the material to order", + }, + "quantity": { + "type": "number", + "description": "Quantity to order", + }, + "priority": { + "type": "string", + "enum": ["normal", "urgent"], + "description": "Order priority", + "default": "normal", + }, + "auto_approve": { + "type": "boolean", + "description": "Auto-approve the order without manual review", + "default": False, + }, + }, + "required": ["material_id", "quantity"], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + material_id = parameters["material_id"] + quantity = parameters["quantity"] + priority = parameters.get("priority", "normal") + auto_approve = parameters.get("auto_approve", False) + + # Check if material exists in inventory + if not hasattr(env, "_inventory"): + env._inventory = {} + + if material_id not in env._inventory: + return ToolResult( + success=False, + error=f"Material '{material_id}' not found in inventory system" + ) + + # Initialize orders tracking + if not hasattr(env, "_material_orders"): + env._material_orders = [] + + # Generate order ID + order_id = str(uuid.uuid4())[:8] + + # Calculate delivery time based on priority + if priority == "urgent": + delivery_days = 1 + cost_multiplier = 1.5 # 50% premium for urgent + else: + delivery_days = 5 + cost_multiplier = 1.0 + + estimated_delivery = datetime.utcnow() + timedelta(days=delivery_days) + + # Estimate cost (simplified pricing model) + material_info = env._inventory[material_id] + base_prices = { + "raw_material": 5.0, # $5 per kg + "components": 2.0, # $2 per unit + "finished_goods": 50.0, # $50 per unit + } + unit_price = base_prices.get(material_info["type"], 1.0) + cost_estimate = quantity * unit_price * cost_multiplier + + # Determine approval status + if auto_approve: + approval_status = "approved" + status = "ordered" + else: + # Auto-approve small orders, require approval for large ones + if cost_estimate < 1000: + approval_status = "auto_approved" + status = "ordered" + else: + approval_status = "pending_approval" + status = "pending" + + # Create order record + order = { + "order_id": order_id, + "material_id": material_id, + "quantity": quantity, + "priority": priority, + "cost_estimate": round(cost_estimate, 2), + "status": status, + "approval_status": approval_status, + "ordered_at": datetime.utcnow().isoformat(), + "estimated_delivery": estimated_delivery.isoformat(), + "delivery_days": delivery_days, + } + + env._material_orders.append(order) + + # Log order if database available + if hasattr(env, "db_manager") and env.db_manager: + try: + if hasattr(env, "current_run_id") and env.current_run_id: + env.db_manager.record_event( + run_id=env.current_run_id, + machine_id="system", + simulation_time=env._state.simulation_time, + event_type="material_order", + data=order + ) + except Exception: + pass + + message = f"Order {order_id} created: {quantity} {material_info['unit']} of {material_id}" + if priority == "urgent": + message += " (URGENT)" + + return ToolResult( + success=True, + data={ + "order_id": order_id, + "estimated_delivery": estimated_delivery.isoformat(), + "delivery_days": delivery_days, + "cost_estimate": round(cost_estimate, 2), + "approval_status": approval_status, + "order_details": order, + "next_steps": [ + f"Order will arrive in {delivery_days} days" if status == "ordered" else "Waiting for approval", + f"Total cost: ${cost_estimate:.2f}", + "Use UpdateStockLevels when materials arrive", + ], + }, + message=message, + ) diff --git a/src/envs/cognitive_manufacturing/tools/predict_maintenance.py b/src/envs/cognitive_manufacturing/tools/predict_maintenance.py new file mode 100644 index 00000000..ba726b2e --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/predict_maintenance.py @@ -0,0 +1,136 @@ +"""Tool for predicting maintenance needs using ML.""" + +from typing import Any, TYPE_CHECKING +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class PredictMaintenanceTool(ManufacturingTool): + """Predict when maintenance will be needed before failures occur.""" + + @property + def name(self) -> str: + return "PredictMaintenance" + + @property + def description(self) -> str: + return "Use ML to predict when maintenance will be needed (predictive maintenance)" + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "machine_id": { + "type": "string", + "description": "Machine identifier", + }, + "prediction_horizon": { + "type": "integer", + "description": "Hours ahead to predict (default: 24)", + "minimum": 1, + "maximum": 168, # 1 week + "default": 24, + }, + "confidence_threshold": { + "type": "number", + "description": "Minimum confidence for recommendation (default: 0.8)", + "minimum": 0.0, + "maximum": 1.0, + "default": 0.8, + }, + }, + "required": ["machine_id"], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + # Check if ML is enabled + if not hasattr(env, "ml_service") or env.ml_service is None: + return ToolResult( + success=False, + error="ML features not enabled. Create environment with enable_ml=True", + ) + + machine_id = parameters["machine_id"] + prediction_horizon = parameters.get("prediction_horizon", 24) + confidence_threshold = parameters.get("confidence_threshold", 0.8) + + # Get machine (support both single and multi-machine mode) + if hasattr(env, "production_line") and env.production_line is not None: + # Multi-machine mode + if machine_id not in env.production_line.machines: + return ToolResult(success=False, error=f"Machine '{machine_id}' not found") + machine = env.production_line.machines[machine_id] + else: + # Single machine mode + machine = env.simulator_machine + if machine.machine_id != machine_id: + return ToolResult(success=False, error=f"Machine '{machine_id}' not found") + + # Get current machine state + temperature = machine.temperature + vibration = machine.vibration + wear_level = machine.wear_level + health_score = machine.health_score + speed = machine.speed + + # Make prediction + prediction = env.ml_service.predict_maintenance_need( + temperature=temperature, + vibration=vibration, + wear_level=wear_level, + health_score=health_score, + speed=speed, + ) + + # Analyze factors + factors = {} + if temperature > 75: + factors["temperature_trend"] = "high" if temperature > 80 else "increasing" + if vibration > 0.3: + factors["vibration_level"] = "high" if vibration > 0.4 else "elevated" + if wear_level > 0.5: + factors["wear_rate"] = "high" if wear_level > 0.7 else "moderate" + if health_score < 70: + factors["health_declining"] = True + + # Generate recommendation + if prediction["maintenance_needed"] and prediction["confidence"] >= confidence_threshold: + if prediction["hours_until_failure"] < 12: + recommendation = f"Schedule immediate maintenance for {machine_id} within next 12 hours" + elif prediction["hours_until_failure"] < prediction_horizon: + recommendation = f"Schedule maintenance for {machine_id} within next {prediction['hours_until_failure']:.0f} hours" + else: + recommendation = f"Monitor {machine_id} closely, maintenance may be needed soon" + else: + recommendation = f"No immediate maintenance needed for {machine_id}" + + return ToolResult( + success=True, + data={ + "machine_id": machine_id, + "maintenance_needed": prediction["maintenance_needed"], + "probability": round(prediction["probability"], 3), + "hours_until_failure": round(prediction["hours_until_failure"], 1), + "confidence": round(prediction["confidence"], 3), + "prediction_horizon": prediction_horizon, + "factors": factors, + "recommendation": recommendation, + "current_state": { + "temperature": round(temperature, 1), + "vibration": round(vibration, 3), + "wear_level": round(wear_level, 3), + "health_score": round(health_score, 1), + }, + "model_method": prediction.get("method", "ml"), + }, + message=f"Maintenance prediction for {machine_id}: " + f"{prediction['probability']:.1%} probability, " + f"{prediction['hours_until_failure']:.0f}h until potential failure", + ) diff --git a/src/envs/cognitive_manufacturing/tools/predict_quality.py b/src/envs/cognitive_manufacturing/tools/predict_quality.py new file mode 100644 index 00000000..8fa1efaf --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/predict_quality.py @@ -0,0 +1,142 @@ +"""Tool for predicting product quality using ML.""" + +from typing import Any, TYPE_CHECKING +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class PredictQualityTool(ManufacturingTool): + """Predict product quality before completion.""" + + @property + def name(self) -> str: + return "PredictQuality" + + @property + def description(self) -> str: + return "Predict product quality based on current production parameters" + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "machine_id": { + "type": "string", + "description": "Machine identifier", + }, + "parameters": { + "type": "object", + "description": "Production parameters to evaluate (optional, uses current if not provided)", + "properties": { + "speed": {"type": "number"}, + "temperature": {"type": "number"}, + }, + }, + }, + "required": ["machine_id"], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + if not hasattr(env, "ml_service") or env.ml_service is None: + return ToolResult( + success=False, + error="ML features not enabled. Create environment with enable_ml=True", + ) + + machine_id = parameters["machine_id"] + custom_params = parameters.get("parameters", {}) + + # Get machine + if hasattr(env, "production_line") and env.production_line is not None: + if machine_id not in env.production_line.machines: + return ToolResult(success=False, error=f"Machine '{machine_id}' not found") + machine = env.production_line.machines[machine_id] + else: + machine = env.simulator_machine + if machine.machine_id != machine_id: + return ToolResult(success=False, error=f"Machine '{machine_id}' not found") + + # Use custom parameters or current state + speed = custom_params.get('speed', machine.speed) + temperature = custom_params.get('temperature', machine.temperature) + vibration = machine.vibration + wear_level = machine.wear_level + + # Predict quality + prediction = env.ml_service.predict_quality( + speed=speed, + temperature=temperature, + vibration=vibration, + wear_level=wear_level, + ) + + # Analyze quality factors + quality_factors = {} + if speed > 85: + quality_factors['speed'] = 'too_high' + elif speed < 40: + quality_factors['speed'] = 'too_low' + else: + quality_factors['speed'] = 'optimal' + + if temperature > 80: + quality_factors['temperature'] = 'too_high' + elif temperature < 60: + quality_factors['temperature'] = 'acceptable' + else: + quality_factors['temperature'] = 'optimal' + + if vibration > 0.4: + quality_factors['vibration'] = 'high' + elif vibration > 0.25: + quality_factors['vibration'] = 'moderate' + else: + quality_factors['vibration'] = 'acceptable' + + # Generate improvement suggestions + improvement_suggestions = [] + if temperature > 75: + improvement_suggestions.append({ + 'parameter': 'temperature', + 'current': round(temperature, 1), + 'recommended': 72.0, + 'quality_gain': round((80 - temperature) / 100, 2), + }) + if speed > 85: + improvement_suggestions.append({ + 'parameter': 'speed', + 'current': round(speed, 1), + 'recommended': 80.0, + 'quality_gain': round((speed - 80) / 200, 2), + }) + + return ToolResult( + success=True, + data={ + "machine_id": machine_id, + "predicted_quality": round(prediction['predicted_quality'], 3), + "confidence_interval": [ + round(prediction['confidence_interval'][0], 3), + round(prediction['confidence_interval'][1], 3), + ], + "pass_probability": round(prediction['pass_probability'], 3), + "quality_factors": quality_factors, + "improvement_suggestions": improvement_suggestions, + "current_parameters": { + "speed": round(speed, 1), + "temperature": round(temperature, 1), + "vibration": round(vibration, 3), + "wear_level": round(wear_level, 3), + }, + "model_method": prediction.get('method', 'ml'), + }, + message=f"Quality prediction for {machine_id}: {prediction['predicted_quality']:.1%} " + f"(pass probability: {prediction['pass_probability']:.1%})", + ) diff --git a/src/envs/cognitive_manufacturing/tools/query_production_history.py b/src/envs/cognitive_manufacturing/tools/query_production_history.py new file mode 100644 index 00000000..4da914e1 --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/query_production_history.py @@ -0,0 +1,153 @@ +"""Tool for querying production history from database.""" + +from typing import Any, TYPE_CHECKING +from datetime import datetime +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class QueryProductionHistoryTool(ManufacturingTool): + """Query historical production data from database.""" + + @property + def name(self) -> str: + return "QueryProductionHistory" + + @property + def description(self) -> str: + return "Query historical production runs with optional filters (date range, reward threshold, status, etc.)" + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "query_type": { + "type": "string", + "enum": ["runs", "recent_runs", "best_runs"], + "description": "Type of query: all runs, recent runs, or best performing runs", + "default": "runs", + }, + "filters": { + "type": "object", + "description": "Optional filters for the query", + "properties": { + "start_date": { + "type": "string", + "description": "Start date in ISO format (e.g., '2024-01-01T00:00:00')", + }, + "end_date": { + "type": "string", + "description": "End date in ISO format", + }, + "min_reward": { + "type": "number", + "description": "Minimum cumulative reward", + }, + "status": { + "type": "string", + "enum": ["in_progress", "completed", "failed"], + "description": "Run status", + }, + "mode": { + "type": "string", + "enum": ["single", "multi_machine"], + "description": "Production mode", + }, + }, + }, + "limit": { + "type": "integer", + "description": "Maximum number of results (default: 100)", + "minimum": 1, + "maximum": 1000, + "default": 100, + }, + }, + "required": [], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + # Check if database is enabled + if not hasattr(env, "db_manager") or env.db_manager is None: + return ToolResult( + success=False, + error="Database not enabled. Create environment with enable_database=True", + ) + + query_type = parameters.get("query_type", "runs") + filters = parameters.get("filters", {}) + limit = parameters.get("limit", 100) + + # Parse date filters if provided + if "start_date" in filters: + try: + filters["start_date"] = datetime.fromisoformat(filters["start_date"].replace("Z", "+00:00")) + except ValueError as e: + return ToolResult(success=False, error=f"Invalid start_date format: {e}") + + if "end_date" in filters: + try: + filters["end_date"] = datetime.fromisoformat(filters["end_date"].replace("Z", "+00:00")) + except ValueError as e: + return ToolResult(success=False, error=f"Invalid end_date format: {e}") + + # Execute query + try: + runs = env.db_manager.query_runs(filters=filters, limit=limit) + + # Apply query type specific processing + if query_type == "recent_runs": + # Already sorted by date desc in query_runs + pass + elif query_type == "best_runs": + # Sort by reward descending + runs = sorted(runs, key=lambda x: x.get("cumulative_reward", 0), reverse=True) + + # Generate summary statistics + if runs: + total_runs = len(runs) + avg_reward = sum(r.get("cumulative_reward", 0) for r in runs) / total_runs + avg_steps = sum(r.get("total_steps", 0) for r in runs) / total_runs + completed_runs = sum(1 for r in runs if r.get("status") == "completed") + + summary = { + "total_runs": total_runs, + "completed_runs": completed_runs, + "avg_reward": round(avg_reward, 2), + "avg_steps": round(avg_steps, 1), + "best_reward": max(r.get("cumulative_reward", 0) for r in runs), + } + else: + summary = { + "total_runs": 0, + "completed_runs": 0, + "avg_reward": 0, + "avg_steps": 0, + "best_reward": 0, + } + + message = f"Found {len(runs)} production runs. Avg reward: {summary['avg_reward']:.2f}" + + return ToolResult( + success=True, + data={ + "query_type": query_type, + "results": runs, + "summary": summary, + "count": len(runs), + }, + message=message, + ) + + except Exception as e: + return ToolResult( + success=False, + error=f"Database query failed: {str(e)}", + ) diff --git a/src/envs/cognitive_manufacturing/tools/read_sensors.py b/src/envs/cognitive_manufacturing/tools/read_sensors.py new file mode 100644 index 00000000..313aa069 --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/read_sensors.py @@ -0,0 +1,68 @@ +"""Tool for reading sensor data from machines.""" + +from typing import Any, TYPE_CHECKING +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class ReadSensorsTool(ManufacturingTool): + """Read real-time sensor data from a machine.""" + + @property + def name(self) -> str: + return "ReadSensors" + + @property + def description(self) -> str: + return "Read current sensor values (temperature, vibration, speed) from a machine" + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "machine_id": {"type": "string", "description": "Machine identifier"}, + "sensors": {"type": "string", "enum": ["all", "temperature", "vibration", "speed"]}, + }, + "required": ["machine_id"], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + machine_id = parameters["machine_id"] + sensors = parameters.get("sensors", "all") + + # Get machine (support both single and multi-machine mode) + if hasattr(env, "production_line") and env.production_line is not None: + # Multi-machine mode: look up machine in production line + if machine_id not in env.production_line.machines: + return ToolResult(success=False, error=f"Machine '{machine_id}' not found") + machine = env.production_line.machines[machine_id] + else: + # Single machine mode + machine = env.simulator_machine + if machine.machine_id != machine_id: + return ToolResult(success=False, error=f"Machine '{machine_id}' not found") + + if sensors == "all": + sensor_data = { + "temperature": round(machine.temperature, 2), + "vibration": round(machine.vibration, 3), + "speed": round(machine.speed, 1), + } + message = f"Read all sensors from {machine_id}" + else: + value = getattr(machine, sensors) + sensor_data = {sensors: round(value, 2)} + message = f"Read {sensors} from {machine_id}: {value:.2f}" + + return ToolResult( + success=True, + data={"machine_id": machine_id, "sensors": sensor_data, "timestamp": env._state.simulation_time}, + message=message, + ) diff --git a/src/envs/cognitive_manufacturing/tools/record_defect.py b/src/envs/cognitive_manufacturing/tools/record_defect.py new file mode 100644 index 00000000..a2bfcd03 --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/record_defect.py @@ -0,0 +1,136 @@ +"""Tool for recording product defects.""" + +from typing import Any, TYPE_CHECKING +import uuid +from datetime import datetime +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class RecordDefectTool(ManufacturingTool): + """Record a product defect in the system.""" + + @property + def name(self) -> str: + return "RecordDefect" + + @property + def description(self) -> str: + return "Record a product defect in the quality management system" + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "unit_id": { + "type": "string", + "description": "ID of the defective unit", + }, + "defect_type": { + "type": "string", + "description": "Type of defect (e.g., 'surface_damage', 'dimensional_error')", + }, + "severity": { + "type": "string", + "enum": ["minor", "major", "critical"], + "description": "Severity level of the defect", + }, + "description": { + "type": "string", + "description": "Detailed description of the defect", + }, + "machine_id": { + "type": "string", + "description": "Machine that produced the defective unit (optional)", + }, + }, + "required": ["unit_id", "defect_type", "severity", "description"], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + unit_id = parameters["unit_id"] + defect_type = parameters["defect_type"] + severity = parameters["severity"] + description = parameters["description"] + machine_id = parameters.get("machine_id") + + # Generate defect ID + defect_id = str(uuid.uuid4())[:8] + recorded_at = datetime.utcnow() + + # Determine if immediate action is required + requires_action = severity == "critical" + + # Store defect in database if available + if hasattr(env, "db_manager") and env.db_manager: + try: + # We would store in defects table, but for now just log as event + if hasattr(env, "current_run_id") and env.current_run_id: + env.db_manager.record_event( + run_id=env.current_run_id, + machine_id=machine_id or "unknown", + simulation_time=env._state.simulation_time, + event_type="defect_recorded", + data={ + "defect_id": defect_id, + "unit_id": unit_id, + "defect_type": defect_type, + "severity": severity, + "description": description, + "recorded_at": recorded_at.isoformat(), + } + ) + except Exception as e: + # Continue even if database recording fails + pass + + # Send alert if critical + if severity == "critical": + alert_msg = f"CRITICAL DEFECT: {defect_type} in {unit_id} - {description}" + env._alerts.append({ + "type": "critical_defect", + "message": alert_msg, + "timestamp": recorded_at.isoformat(), + "defect_id": defect_id, + }) + + # Determine recommended actions + recommended_actions = [] + if severity == "critical": + recommended_actions.append("Halt production and inspect machine") + recommended_actions.append("Quarantine all recent units from same machine") + if machine_id: + recommended_actions.append(f"Schedule immediate maintenance for {machine_id}") + elif severity == "major": + recommended_actions.append("Increase inspection frequency") + recommended_actions.append("Review production parameters") + else: # minor + recommended_actions.append("Monitor for pattern of similar defects") + + message = f"Defect recorded: {defect_id} - {severity.upper()} {defect_type} in {unit_id}" + + return ToolResult( + success=True, + data={ + "defect_id": defect_id, + "recorded_at": recorded_at.isoformat(), + "requires_action": requires_action, + "recommended_actions": recommended_actions, + "alert_sent": severity == "critical", + "details": { + "unit_id": unit_id, + "defect_type": defect_type, + "severity": severity, + "description": description, + "machine_id": machine_id, + }, + }, + message=message, + ) diff --git a/src/envs/cognitive_manufacturing/tools/save_production_data.py b/src/envs/cognitive_manufacturing/tools/save_production_data.py new file mode 100644 index 00000000..51068111 --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/save_production_data.py @@ -0,0 +1,117 @@ +"""Tool for saving production data to database.""" + +from typing import Any, TYPE_CHECKING +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class SaveProductionDataTool(ManufacturingTool): + """Save current production run data to database.""" + + @property + def name(self) -> str: + return "SaveProductionData" + + @property + def description(self) -> str: + return "Save the current production run data to the database for later analysis" + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "run_name": { + "type": "string", + "description": "Optional name for this production run", + }, + "metadata": { + "type": "object", + "description": "Optional metadata (tags, notes, experiment info)", + }, + }, + "required": [], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + # Check if database is enabled + if not hasattr(env, "db_manager") or env.db_manager is None: + return ToolResult( + success=False, + error="Database not enabled. Create environment with enable_database=True", + ) + + run_name = parameters.get("run_name") + metadata = parameters.get("metadata", {}) + + # If run doesn't exist yet, create it + if not hasattr(env, "current_run_id") or env.current_run_id is None: + mode = "multi_machine" if env.multi_machine else "single" + run_id = env.db_manager.create_run( + mode=mode, + run_name=run_name, + metadata=metadata, + ) + env.current_run_id = run_id + else: + run_id = env.current_run_id + + # Save current state + state = env._state + env.db_manager.complete_run( + run_id=run_id, + total_steps=state.step_count, + simulation_time=state.simulation_time, + cumulative_reward=env._cumulative_reward, + ) + + # Save all sensor readings (collect from current machines) + if env.multi_machine: + # Save from all machines + for machine_id, machine in env.production_line.machines.items(): + env.db_manager.save_sensor_reading( + run_id=run_id, + machine_id=machine_id, + simulation_time=state.simulation_time, + temperature=machine.temperature, + vibration=machine.vibration, + speed=machine.speed, + health_score=machine.health_score, + wear_level=machine.wear_level, + production_output=machine.production_output, + status=machine.status, + ) + else: + # Save from single machine + machine = env.simulator_machine + env.db_manager.save_sensor_reading( + run_id=run_id, + machine_id=machine.machine_id, + simulation_time=state.simulation_time, + temperature=machine.temperature, + vibration=machine.vibration, + speed=machine.speed, + health_score=machine.health_score, + wear_level=machine.wear_level, + production_output=machine.production_output, + status=machine.status, + ) + + return ToolResult( + success=True, + data={ + "run_id": run_id, + "run_name": run_name, + "total_steps": state.step_count, + "simulation_time": state.simulation_time, + "cumulative_reward": env._cumulative_reward, + "machines_saved": len(env.production_line.machines) if env.multi_machine else 1, + }, + message=f"Saved production run '{run_name or run_id}' to database", + ) diff --git a/src/envs/cognitive_manufacturing/tools/schedule_maintenance.py b/src/envs/cognitive_manufacturing/tools/schedule_maintenance.py new file mode 100644 index 00000000..fc7ae73b --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/schedule_maintenance.py @@ -0,0 +1,102 @@ +"""Tool for scheduling machine maintenance.""" + +from typing import Any, TYPE_CHECKING +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class ScheduleMaintenanceTool(ManufacturingTool): + """Schedule or perform maintenance on a machine.""" + + @property + def name(self) -> str: + return "ScheduleMaintenance" + + @property + def description(self) -> str: + return "Schedule or perform immediate maintenance on a machine. Resets wear and health." + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "machine_id": {"type": "string", "description": "Machine identifier"}, + "maintenance_type": { + "type": "string", + "enum": ["immediate", "scheduled"], + "description": "Whether to perform maintenance now or schedule it", + }, + "reason": {"type": "string", "description": "Reason for maintenance"}, + }, + "required": ["machine_id", "maintenance_type"], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + machine_id = parameters["machine_id"] + maintenance_type = parameters["maintenance_type"] + reason = parameters.get("reason", "Routine maintenance") + + # Get machine (support both single and multi-machine mode) + if hasattr(env, "production_line") and env.production_line is not None: + # Multi-machine mode: look up machine in production line + if machine_id not in env.production_line.machines: + return ToolResult(success=False, error=f"Machine '{machine_id}' not found") + machine = env.production_line.machines[machine_id] + else: + # Single machine mode + machine = env.simulator_machine + if machine.machine_id != machine_id: + return ToolResult(success=False, error=f"Machine '{machine_id}' not found") + + if maintenance_type == "immediate": + # Perform maintenance now + old_health = machine.health_score + old_wear = machine.wear_level + old_status = machine.status + + # Reset machine to healthy state + machine.perform_maintenance() + + return ToolResult( + success=True, + data={ + "machine_id": machine_id, + "maintenance_type": "immediate", + "reason": reason, + "before": { + "health_score": round(old_health, 1), + "wear_level": round(old_wear, 3), + "status": old_status, + }, + "after": { + "health_score": round(machine.health_score, 1), + "wear_level": round(machine.wear_level, 3), + "status": machine.status, + }, + "estimated_downtime": 2.0, # hours + }, + message=f"Performed immediate maintenance on {machine_id}. Health restored from {old_health:.1f} to {machine.health_score:.1f}", + ) + else: + # Schedule maintenance for later + scheduled_time = env._state.simulation_time + 8.0 # Schedule 8 hours ahead + + return ToolResult( + success=True, + data={ + "machine_id": machine_id, + "maintenance_type": "scheduled", + "reason": reason, + "scheduled_time": scheduled_time, + "current_time": env._state.simulation_time, + "estimated_downtime": 2.0, + }, + message=f"Scheduled maintenance for {machine_id} at time {scheduled_time:.1f}. Reason: {reason}", + ) diff --git a/src/envs/cognitive_manufacturing/tools/search_knowledge.py b/src/envs/cognitive_manufacturing/tools/search_knowledge.py new file mode 100644 index 00000000..385bdf5c --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/search_knowledge.py @@ -0,0 +1,115 @@ +"""Tool for searching knowledge base using semantic search.""" + +from typing import Any, TYPE_CHECKING +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class SearchKnowledgeTool(ManufacturingTool): + """Search knowledge base using semantic similarity.""" + + @property + def name(self) -> str: + return "SearchKnowledge" + + @property + def description(self) -> str: + return "Search knowledge base using natural language query (semantic search for troubleshooting, maintenance, safety info)" + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Natural language query (e.g., 'how to fix overheating', 'maintenance schedule')", + }, + "doc_type": { + "type": "string", + "enum": ["maintenance", "troubleshooting", "safety", "procedure", "general"], + "description": "Optional filter by document type", + }, + "top_k": { + "type": "integer", + "description": "Number of results to return (default: 5)", + "minimum": 1, + "maximum": 20, + "default": 5, + }, + }, + "required": ["query"], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + # Check if database and embedding service are enabled + if not hasattr(env, "db_manager") or env.db_manager is None: + return ToolResult( + success=False, + error="Database not enabled. Create environment with enable_database=True", + ) + + if not hasattr(env, "embedding_service") or env.embedding_service is None: + return ToolResult( + success=False, + error="Embedding service not enabled", + ) + + query = parameters["query"] + doc_type = parameters.get("doc_type") + top_k = parameters.get("top_k", 5) + + try: + # Generate query embedding + query_embedding = env.embedding_service.embed_text(query) + + # Search knowledge base + results = env.db_manager.search_knowledge( + query_embedding=query_embedding, + doc_type=doc_type, + top_k=top_k, + ) + + # Format results + formatted_results = [] + for result in results: + formatted_results.append({ + "doc_id": result["doc_id"], + "title": result["title"], + "content": result["content"], + "doc_type": result["doc_type"], + "similarity": round(result["similarity"], 4), + "metadata": result.get("metadata"), + }) + + if not results: + message = f"No results found for query: '{query}'" + else: + best_match = results[0] + message = ( + f"Found {len(results)} results. " + f"Best match: '{best_match['title']}' ({best_match['similarity']:.1%} similar)" + ) + + return ToolResult( + success=True, + data={ + "query": query, + "results": formatted_results, + "result_count": len(results), + "doc_type_filter": doc_type, + }, + message=message, + ) + + except Exception as e: + return ToolResult( + success=False, + error=f"Knowledge search failed: {str(e)}", + ) diff --git a/src/envs/cognitive_manufacturing/tools/send_alert.py b/src/envs/cognitive_manufacturing/tools/send_alert.py new file mode 100644 index 00000000..5c780115 --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/send_alert.py @@ -0,0 +1,82 @@ +"""Tool for sending alerts to operators.""" + +import uuid +from typing import Any, TYPE_CHECKING +from .base import ManufacturingTool, ToolResult +from ..models import Alert + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class SendAlertTool(ManufacturingTool): + """Send an alert notification to operators.""" + + @property + def name(self) -> str: + return "SendAlert" + + @property + def description(self) -> str: + return "Send an alert notification to operators about machine status or issues" + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "machine_id": {"type": "string", "description": "Machine identifier"}, + "severity": { + "type": "string", + "enum": ["info", "warning", "critical"], + "description": "Alert severity level", + }, + "message": {"type": "string", "description": "Alert message content"}, + "category": { + "type": "string", + "enum": ["temperature", "vibration", "wear", "health", "production", "safety", "other"], + "description": "Alert category", + }, + }, + "required": ["machine_id", "severity", "message"], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + machine_id = parameters["machine_id"] + severity = parameters["severity"] + message = parameters["message"] + category = parameters.get("category", "other") + machine = env.simulator_machine + + if machine.machine_id != machine_id: + return ToolResult(success=False, error=f"Machine '{machine_id}' not found") + + # Create alert + alert = Alert( + alert_id=str(uuid.uuid4()), + machine_id=machine_id, + severity=severity, + message=message, + category=category, + timestamp=env._state.simulation_time, + ) + + # Add to environment's alert list + env._alerts.append(alert) + + return ToolResult( + success=True, + data={ + "alert_id": alert.alert_id, + "machine_id": machine_id, + "severity": severity, + "category": category, + "message": message, + "timestamp": alert.timestamp, + }, + message=f"Sent {severity} alert for {machine_id}: {message}", + ) diff --git a/src/envs/cognitive_manufacturing/tools/set_power_mode.py b/src/envs/cognitive_manufacturing/tools/set_power_mode.py new file mode 100644 index 00000000..749f0ef6 --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/set_power_mode.py @@ -0,0 +1,162 @@ +"""Tool for adjusting machine power modes.""" + +from typing import Any, TYPE_CHECKING +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class SetPowerModeTool(ManufacturingTool): + """Adjust machine power consumption modes.""" + + @property + def name(self) -> str: + return "SetPowerMode" + + @property + def description(self) -> str: + return "Adjust machine power consumption modes for energy optimization" + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "machine_id": { + "type": "string", + "description": "Machine ID or 'all' for all machines", + }, + "power_mode": { + "type": "string", + "enum": ["eco", "normal", "high_performance"], + "description": "Power mode: eco (low power), normal (balanced), high_performance (max power)", + }, + "schedule": { + "type": "object", + "description": "Optional schedule for power mode changes", + "properties": { + "start_hour": {"type": "integer"}, + "end_hour": {"type": "integer"}, + }, + }, + }, + "required": ["machine_id", "power_mode"], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + machine_id = parameters["machine_id"] + power_mode = parameters["power_mode"] + schedule = parameters.get("schedule") + + # Get machines to update + if hasattr(env, "production_line") and env.production_line is not None: + if machine_id == "all": + machines_to_update = list(env.production_line.machines.values()) + machine_ids = list(env.production_line.machines.keys()) + elif machine_id in env.production_line.machines: + machines_to_update = [env.production_line.machines[machine_id]] + machine_ids = [machine_id] + else: + return ToolResult(success=False, error=f"Machine '{machine_id}' not found") + else: + if machine_id == "all" or machine_id == env.machine_id: + machines_to_update = [env.simulator_machine] + machine_ids = [env.machine_id] + else: + return ToolResult(success=False, error=f"Machine '{machine_id}' not found") + + # Power mode effects + mode_effects = { + "eco": { + "power_savings_pct": 20.0, + "performance_impact_pct": -10.0, # 10% slower + "description": "Reduced power consumption, slightly lower throughput", + }, + "normal": { + "power_savings_pct": 0.0, + "performance_impact_pct": 0.0, + "description": "Balanced power and performance", + }, + "high_performance": { + "power_savings_pct": -20.0, # 20% more power + "performance_impact_pct": 15.0, # 15% faster + "description": "Maximum performance, higher power consumption", + }, + } + + # Apply power mode to machines + for machine in machines_to_update: + machine._power_mode = power_mode + + # Adjust speed if in eco or high_performance mode + if power_mode == "eco": + # Eco mode: slightly reduce max speed + machine._power_mode_speed_multiplier = 0.9 + elif power_mode == "high_performance": + # High performance: allow higher speeds + machine._power_mode_speed_multiplier = 1.15 + else: # normal + machine._power_mode_speed_multiplier = 1.0 + + # Get mode effects + effects = mode_effects[power_mode] + + # Log the change if database available + if hasattr(env, "db_manager") and env.db_manager: + try: + if hasattr(env, "current_run_id") and env.current_run_id: + env.db_manager.record_event( + run_id=env.current_run_id, + machine_id=machine_id, + simulation_time=env._state.simulation_time, + event_type="power_mode_change", + data={ + "machines": machine_ids, + "power_mode": power_mode, + "schedule": schedule, + "effects": effects, + } + ) + except Exception: + pass + + # Generate impact summary + annual_hours = 8760 # Hours per year + avg_power_kw = 25.0 # Average power per machine + electricity_rate = 0.12 # $/kWh + + savings_per_machine = (avg_power_kw * annual_hours * abs(effects["power_savings_pct"]) / 100.0) * electricity_rate + total_annual_savings = savings_per_machine * len(machines_to_update) + + if effects["power_savings_pct"] > 0: + impact_msg = f"Expected annual savings: ${total_annual_savings:.0f}" + elif effects["power_savings_pct"] < 0: + impact_msg = f"Expected annual cost increase: ${abs(total_annual_savings):.0f}" + else: + impact_msg = "No change in power consumption" + + message = f"Power mode set to '{power_mode}' for {len(machines_to_update)} machine(s)" + + return ToolResult( + success=True, + data={ + "applied": True, + "power_mode": power_mode, + "machines_affected": machine_ids, + "expected_savings_pct": effects["power_savings_pct"], + "performance_impact_pct": effects["performance_impact_pct"], + "description": effects["description"], + "impact_summary": { + "annual_savings_usd": round(total_annual_savings, 2) if effects["power_savings_pct"] > 0 else round(-total_annual_savings, 2), + "monthly_savings_usd": round(total_annual_savings / 12, 2) if effects["power_savings_pct"] > 0 else round(-total_annual_savings / 12, 2), + "impact_message": impact_msg, + }, + "schedule": schedule if schedule else "immediate", + }, + message=message, + ) diff --git a/src/envs/cognitive_manufacturing/tools/simulate_scenario.py b/src/envs/cognitive_manufacturing/tools/simulate_scenario.py new file mode 100644 index 00000000..2d7a6a74 --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/simulate_scenario.py @@ -0,0 +1,200 @@ +"""Tool for running what-if scenario simulations.""" + +from typing import Any, TYPE_CHECKING +import uuid +import copy +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class SimulateScenarioTool(ManufacturingTool): + """Run what-if scenario simulations to test parameter changes.""" + + @property + def name(self) -> str: + return "SimulateScenario" + + @property + def description(self) -> str: + return "Run what-if scenario simulations to predict outcomes of parameter changes" + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "scenario_name": { + "type": "string", + "description": "Name for this scenario", + }, + "changes": { + "type": "object", + "description": "Parameter changes to test (e.g., {\"speed\": 80, \"temperature\": 75})", + }, + "simulation_steps": { + "type": "integer", + "description": "Number of simulation steps to run (default: 100)", + "default": 100, + }, + "metrics_to_track": { + "type": "array", + "items": {"type": "string"}, + "description": "Metrics to track (default: [\"throughput\", \"quality\", \"cost\", \"energy\"])", + "default": ["throughput", "quality", "cost", "energy"], + }, + }, + "required": ["scenario_name", "changes"], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + scenario_name = parameters["scenario_name"] + changes = parameters["changes"] + simulation_steps = parameters.get("simulation_steps", 100) + metrics_to_track = parameters.get("metrics_to_track", ["throughput", "quality", "cost", "energy"]) + + scenario_id = str(uuid.uuid4())[:8] + + # Capture baseline state + if hasattr(env, "production_line") and env.production_line is not None: + baseline_state = { + "avg_speed": sum(m.speed for m in env.production_line.machines.values()) / len(env.production_line.machines), + "avg_temperature": sum(m.temperature for m in env.production_line.machines.values()) / len(env.production_line.machines), + "total_units": sum(m.units_produced for m in env.production_line.machines.values()), + "avg_health": sum(m.health_score for m in env.production_line.machines.values()) / len(env.production_line.machines), + } + else: + baseline_state = { + "speed": env.simulator_machine.speed, + "temperature": env.simulator_machine.temperature, + "units": env.simulator_machine.units_produced, + "health": env.simulator_machine.health_score, + } + + # Simulate the scenario (simplified simulation) + # In a real implementation, we would create a copy of the environment and run it + # For now, we'll use estimates based on the changes + + # Estimate impacts + scenario_results = { + "throughput": 0.0, + "quality": 0.0, + "cost": 0.0, + "energy": 0.0, + "safety": 0.0, + } + + # Speed impact + if "speed" in changes: + speed_change = changes["speed"] - baseline_state.get("avg_speed", baseline_state.get("speed", 70)) + # Higher speed = higher throughput, slightly lower quality, higher energy + scenario_results["throughput"] = speed_change * 0.8 # 80% throughput increase per speed unit + scenario_results["quality"] = -abs(speed_change) * 0.3 # Quality degrades with speed changes + scenario_results["energy"] = speed_change * 0.5 # Energy increases with speed + + # Temperature impact + if "temperature" in changes: + temp = changes["temperature"] + optimal_temp = 70.0 + temp_deviation = abs(temp - optimal_temp) + scenario_results["quality"] -= temp_deviation * 0.5 # Quality degrades away from optimal + if temp > 85: + scenario_results["safety"] -= (temp - 85) * 2.0 # Safety risk at high temps + + # Maintenance impact + if "maintenance_frequency" in changes: + freq = changes["maintenance_frequency"] + scenario_results["cost"] += freq * 100 # Maintenance cost + scenario_results["quality"] += freq * 2 # Better quality with more maintenance + scenario_results["throughput"] -= freq * 5 # Downtime from maintenance + + # Power mode impact + if "power_mode" in changes: + mode = changes["power_mode"] + if mode == "eco": + scenario_results["energy"] -= 20 + scenario_results["throughput"] -= 10 + elif mode == "high_performance": + scenario_results["energy"] += 20 + scenario_results["throughput"] += 15 + + # Estimate absolute values (starting from current state) + estimated_throughput = baseline_state.get("total_units", baseline_state.get("units", 100)) * (1.0 + scenario_results["throughput"] / 100.0) + estimated_quality = max(0, min(100, 85 + scenario_results["quality"])) + estimated_cost = 5000 * (1.0 + scenario_results["cost"] / 100.0) # Base cost of $5000 + estimated_energy = 1000 * (1.0 + scenario_results["energy"] / 100.0) # Base energy 1000 kWh + + # Compare to baseline + baseline_throughput = baseline_state.get("total_units", baseline_state.get("units", 100)) + baseline_quality = 85.0 + baseline_cost = 5000.0 + baseline_energy = 1000.0 + + comparison = { + "throughput": { + "baseline": baseline_throughput, + "scenario": round(estimated_throughput, 1), + "change_pct": round(scenario_results["throughput"], 1), + }, + "quality": { + "baseline": baseline_quality, + "scenario": round(estimated_quality, 1), + "change_pct": round(scenario_results["quality"], 1), + }, + "cost": { + "baseline": baseline_cost, + "scenario": round(estimated_cost, 2), + "change_pct": round(scenario_results["cost"], 1), + }, + "energy": { + "baseline": baseline_energy, + "scenario": round(estimated_energy, 1), + "change_pct": round(scenario_results["energy"], 1), + }, + } + + # Generate recommendations + recommendations = [] + + if scenario_results["throughput"] > 5: + recommendations.append("✅ Throughput improvement expected - consider implementing") + if scenario_results["quality"] < -5: + recommendations.append("⚠️ Quality degradation risk - review quality controls") + if scenario_results["energy"] > 10: + recommendations.append("⚠️ Significant energy increase - evaluate cost-benefit") + if scenario_results["safety"] < -5: + recommendations.append("❌ Safety concerns - do not implement without additional controls") + + if not recommendations: + recommendations.append("Scenario has minimal impact - changes are safe to implement") + + message = f"Scenario '{scenario_name}' simulated: {len(changes)} parameters changed" + + return ToolResult( + success=True, + data={ + "scenario_id": scenario_id, + "scenario_name": scenario_name, + "changes_applied": changes, + "simulation_results": scenario_results, + "estimated_outcomes": { + "throughput_units": round(estimated_throughput, 1), + "quality_pct": round(estimated_quality, 1), + "cost_usd": round(estimated_cost, 2), + "energy_kwh": round(estimated_energy, 1), + }, + "comparison_to_baseline": comparison, + "recommendations": recommendations, + "simulation_details": { + "steps_simulated": simulation_steps, + "metrics_tracked": metrics_to_track, + "baseline_state": baseline_state, + }, + }, + message=message, + ) diff --git a/src/envs/cognitive_manufacturing/tools/transfer_material.py b/src/envs/cognitive_manufacturing/tools/transfer_material.py new file mode 100644 index 00000000..c5df44a5 --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/transfer_material.py @@ -0,0 +1,127 @@ +"""Tool for manually transferring material between machines.""" + +from typing import Any, TYPE_CHECKING +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class TransferMaterialTool(ManufacturingTool): + """Manually trigger material transfer between machines.""" + + @property + def name(self) -> str: + return "TransferMaterial" + + @property + def description(self) -> str: + return "Manually transfer material units between machines through buffers" + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "from_machine": { + "type": "string", + "description": "Source machine ID (M1, M2, M3, M4)", + "enum": ["M1", "M2", "M3", "M4"], + }, + "to_machine": { + "type": "string", + "description": "Destination machine ID (M1, M2, M3, M4)", + "enum": ["M1", "M2", "M3", "M4"], + }, + "units": { + "type": "integer", + "description": "Number of units to transfer", + "minimum": 1, + }, + }, + "required": ["from_machine", "to_machine", "units"], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + from_machine = parameters["from_machine"] + to_machine = parameters["to_machine"] + units = parameters["units"] + + # Check if environment has production line simulator + if not hasattr(env, "production_line") or env.production_line is None: + return ToolResult( + success=False, + error="TransferMaterial only works in production line mode (multi-machine)", + ) + + production_line = env.production_line + + # Validate machines exist + if from_machine not in production_line.machines: + return ToolResult(success=False, error=f"Source machine '{from_machine}' not found") + + if to_machine not in production_line.machines: + return ToolResult(success=False, error=f"Destination machine '{to_machine}' not found") + + # Check if machines are adjacent (can only transfer between adjacent machines) + valid_transfers = { + ("M1", "M2"): "M1_M2", + ("M2", "M3"): "M2_M3", + ("M3", "M4"): "M3_M4", + } + + transfer_key = (from_machine, to_machine) + if transfer_key not in valid_transfers: + return ToolResult( + success=False, + error=f"Cannot transfer from {from_machine} to {to_machine}. " + f"Only adjacent machines can transfer: M1→M2, M2→M3, M3→M4", + ) + + buffer_id = valid_transfers[transfer_key] + buffer = production_line.buffers[buffer_id] + + # Check if source buffer has enough units + if buffer.current_level < units: + return ToolResult( + success=False, + error=f"Insufficient units in buffer {buffer_id}. " + f"Requested: {units}, Available: {buffer.current_level}", + ) + + # Check if destination can accept units + # For simplicity, we'll just move units within the buffer system + # The actual processing will happen in the normal step() cycle + + # Remove units from source side + removed_units = buffer.remove_units(units) + transferred = len(removed_units) + + # Add to the next buffer (if exists) or mark as processed + if to_machine == "M4": + # M4 is final stage, units would go to finished products + # But manual transfer to M4 doesn't make sense in production flow + # Re-add units back + for unit in removed_units: + buffer.add_unit(unit) + return ToolResult( + success=False, + error="Cannot manually transfer to M4 (QC stage). Units flow automatically.", + ) + + return ToolResult( + success=True, + data={ + "transferred": transferred, + "from_machine": from_machine, + "to_machine": to_machine, + "buffer_id": buffer_id, + "buffer_level_after": buffer.current_level, + "buffer_capacity": buffer.capacity, + }, + message=f"Transferred {transferred} units from {from_machine} to {to_machine} (buffer: {buffer_id})", + ) diff --git a/src/envs/cognitive_manufacturing/tools/update_qc_thresholds.py b/src/envs/cognitive_manufacturing/tools/update_qc_thresholds.py new file mode 100644 index 00000000..4a63eff1 --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/update_qc_thresholds.py @@ -0,0 +1,147 @@ +"""Tool for updating quality control thresholds.""" + +from typing import Any, TYPE_CHECKING +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class UpdateQCThresholdsTool(ManufacturingTool): + """Update quality control thresholds and parameters.""" + + @property + def name(self) -> str: + return "UpdateQCThresholds" + + @property + def description(self) -> str: + return "Update quality control thresholds and inspection parameters" + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "threshold_type": { + "type": "string", + "enum": ["quality_min", "defect_rate_max", "inspection_frequency", "temperature_max", "vibration_max"], + "description": "Type of threshold to update", + }, + "new_value": { + "type": "number", + "description": "New threshold value", + }, + "apply_to": { + "type": "string", + "description": "Apply to 'all' machines or specific machine_id", + "default": "all", + }, + }, + "required": ["threshold_type", "new_value"], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + threshold_type = parameters["threshold_type"] + new_value = parameters["new_value"] + apply_to = parameters.get("apply_to", "all") + + # Initialize QC config if not exists + if not hasattr(env, "_qc_config"): + env._qc_config = { + "quality_min": 70.0, + "defect_rate_max": 0.05, # 5% + "inspection_frequency": 10, # Every 10 units + "temperature_max": 85.0, + "vibration_max": 10.0, + } + + # Get old value + old_value = env._qc_config.get(threshold_type, 0.0) + + # Validate new value + if threshold_type == "quality_min": + if not (0 <= new_value <= 100): + return ToolResult(success=False, error="quality_min must be between 0 and 100") + elif threshold_type == "defect_rate_max": + if not (0 <= new_value <= 1): + return ToolResult(success=False, error="defect_rate_max must be between 0 and 1") + elif threshold_type == "inspection_frequency": + if new_value < 1: + return ToolResult(success=False, error="inspection_frequency must be >= 1") + + # Update threshold + env._qc_config[threshold_type] = new_value + + # Determine affected machines + if hasattr(env, "production_line") and env.production_line is not None: + if apply_to == "all": + affected_machines = list(env.production_line.machines.keys()) + else: + affected_machines = [apply_to] if apply_to in env.production_line.machines else [] + else: + affected_machines = [env.machine_id] if apply_to == "all" or apply_to == env.machine_id else [] + + # Calculate impact + change_pct = ((new_value - old_value) / old_value * 100) if old_value != 0 else 100.0 + + # Determine impact description + impact_description = [] + if threshold_type == "quality_min": + if new_value > old_value: + impact_description.append("Higher quality standards - may reduce throughput") + impact_description.append("Expected defect rate decrease") + else: + impact_description.append("Lower quality standards - may increase throughput") + impact_description.append("Expected defect rate may increase") + elif threshold_type == "defect_rate_max": + if new_value > old_value: + impact_description.append("More tolerant of defects") + else: + impact_description.append("Less tolerant of defects - stricter quality control") + elif threshold_type == "inspection_frequency": + if new_value > old_value: + impact_description.append("Less frequent inspections - faster throughput") + else: + impact_description.append("More frequent inspections - better quality detection") + + # Log the change if database available + if hasattr(env, "db_manager") and env.db_manager: + try: + if hasattr(env, "current_run_id") and env.current_run_id: + env.db_manager.record_event( + run_id=env.current_run_id, + machine_id="system", + simulation_time=env._state.simulation_time, + event_type="qc_threshold_update", + data={ + "threshold_type": threshold_type, + "old_value": old_value, + "new_value": new_value, + "apply_to": apply_to, + "affected_machines": affected_machines, + } + ) + except Exception: + pass + + message = f"Updated {threshold_type}: {old_value} → {new_value} ({change_pct:+.1f}%)" + + return ToolResult( + success=True, + data={ + "updated": True, + "threshold_type": threshold_type, + "old_value": round(old_value, 2), + "new_value": round(new_value, 2), + "change_percent": round(change_pct, 1), + "affected_machines": affected_machines, + "impact_description": impact_description, + "current_qc_config": env._qc_config.copy(), + }, + message=message, + ) diff --git a/src/envs/cognitive_manufacturing/tools/update_stock_levels.py b/src/envs/cognitive_manufacturing/tools/update_stock_levels.py new file mode 100644 index 00000000..9c5f63f7 --- /dev/null +++ b/src/envs/cognitive_manufacturing/tools/update_stock_levels.py @@ -0,0 +1,156 @@ +"""Tool for updating inventory stock levels.""" + +from typing import Any, TYPE_CHECKING +import uuid +from datetime import datetime +from .base import ManufacturingTool, ToolResult + +if TYPE_CHECKING: + from ..server.environment import CognitiveManufacturingEnvironment + + +class UpdateStockLevelsTool(ManufacturingTool): + """Update material inventory stock levels.""" + + @property + def name(self) -> str: + return "UpdateStockLevels" + + @property + def description(self) -> str: + return "Update material inventory stock levels (add, remove, or set)" + + @property + def parameters_schema(self) -> dict: + return { + "type": "object", + "properties": { + "material_id": { + "type": "string", + "description": "ID of the material to update", + }, + "change_type": { + "type": "string", + "enum": ["add", "remove", "set"], + "description": "Type of update: add to stock, remove from stock, or set absolute level", + }, + "quantity": { + "type": "number", + "description": "Quantity to add/remove or new absolute level", + }, + "reason": { + "type": "string", + "description": "Reason for stock update", + }, + }, + "required": ["material_id", "change_type", "quantity", "reason"], + } + + def execute(self, parameters: dict[str, Any], env: "CognitiveManufacturingEnvironment") -> ToolResult: + is_valid, error = self.validate_parameters(parameters) + if not is_valid: + return ToolResult(success=False, error=error) + + material_id = parameters["material_id"] + change_type = parameters["change_type"] + quantity = parameters["quantity"] + reason = parameters["reason"] + + # Initialize inventory if not exists + if not hasattr(env, "_inventory"): + env._inventory = {} + + if material_id not in env._inventory: + return ToolResult( + success=False, + error=f"Material '{material_id}' not found in inventory system" + ) + + # Get old level + old_level = env._inventory[material_id]["level"] + + # Calculate new level based on change type + if change_type == "add": + new_level = old_level + quantity + elif change_type == "remove": + new_level = old_level - quantity + if new_level < 0: + return ToolResult( + success=False, + error=f"Insufficient stock: trying to remove {quantity}, but only {old_level} available" + ) + else: # set + new_level = quantity + + # Update the level + env._inventory[material_id]["level"] = new_level + + # Generate transaction ID + transaction_id = str(uuid.uuid4())[:8] + + # Initialize transaction history + if not hasattr(env, "_inventory_transactions"): + env._inventory_transactions = [] + + # Record transaction + transaction = { + "transaction_id": transaction_id, + "material_id": material_id, + "change_type": change_type, + "quantity": quantity, + "old_level": old_level, + "new_level": new_level, + "reason": reason, + "timestamp": datetime.utcnow().isoformat(), + } + + env._inventory_transactions.append(transaction) + + # Log transaction if database available + if hasattr(env, "db_manager") and env.db_manager: + try: + if hasattr(env, "current_run_id") and env.current_run_id: + env.db_manager.record_event( + run_id=env.current_run_id, + machine_id="system", + simulation_time=env._state.simulation_time, + event_type="inventory_update", + data=transaction + ) + except Exception: + pass + + # Check if stock is now low + reorder_point = env._inventory[material_id]["reorder_point"] + stock_status = "ok" + warnings = [] + + if new_level < reorder_point: + stock_status = "low" + warnings.append(f"Stock below reorder point ({new_level:.0f} < {reorder_point:.0f})") + + if new_level < reorder_point * 0.5: + stock_status = "critical" + warnings.append("Stock critically low - consider urgent reorder") + + # Calculate change percentage + change_pct = ((new_level - old_level) / old_level * 100) if old_level != 0 else 100.0 + + message = f"Stock updated: {material_id} {change_type} {quantity} ({old_level:.0f} → {new_level:.0f})" + + return ToolResult( + success=True, + data={ + "updated": True, + "transaction_id": transaction_id, + "material_id": material_id, + "old_level": round(old_level, 2), + "new_level": round(new_level, 2), + "change": round(new_level - old_level, 2), + "change_percent": round(change_pct, 1), + "stock_status": stock_status, + "warnings": warnings, + "transaction_details": transaction, + }, + message=message, + )