From d402171333a9bafe8f0076a56413d70f98a229e0 Mon Sep 17 00:00:00 2001 From: Piotr Kolakowski Date: Wed, 10 Dec 2025 19:16:19 +0100 Subject: [PATCH] Add Calibration Dispatcher Agent - production-grade coded agent for medical device calibration scheduling --- .../calibration-dispatcher-agent/.DS_Store | Bin 0 -> 6148 bytes .../calibration-dispatcher-agent/.env.example | 11 + .../calibration-dispatcher-agent/README.md | 384 +++++ .../calibration-dispatcher-agent/config.py | 216 +++ .../data/README.md | 59 + .../data/Schema.json | 1002 +++++++++++++ .../data/devices_for_data_fabric.csv | 21 + .../data/locations.csv | 21 + .../data/technicians.csv | 6 + samples/calibration-dispatcher-agent/main.py | 1272 +++++++++++++++++ .../mcp_bridge.py | 316 ++++ .../policies/Calibration_Rules_Document.pdf | Bin 0 -> 309049 bytes .../policies/README.md | 114 ++ .../policies/Routing_Guidelines_Document.pdf | Bin 0 -> 453848 bytes .../policies/Service_Procedures_Document.pdf | Bin 0 -> 292455 bytes .../requirements.txt | 24 + 16 files changed, 3446 insertions(+) create mode 100644 samples/calibration-dispatcher-agent/.DS_Store create mode 100644 samples/calibration-dispatcher-agent/.env.example create mode 100644 samples/calibration-dispatcher-agent/README.md create mode 100644 samples/calibration-dispatcher-agent/config.py create mode 100644 samples/calibration-dispatcher-agent/data/README.md create mode 100644 samples/calibration-dispatcher-agent/data/Schema.json create mode 100644 samples/calibration-dispatcher-agent/data/devices_for_data_fabric.csv create mode 100644 samples/calibration-dispatcher-agent/data/locations.csv create mode 100644 samples/calibration-dispatcher-agent/data/technicians.csv create mode 100644 samples/calibration-dispatcher-agent/main.py create mode 100644 samples/calibration-dispatcher-agent/mcp_bridge.py create mode 100644 samples/calibration-dispatcher-agent/policies/Calibration_Rules_Document.pdf create mode 100644 samples/calibration-dispatcher-agent/policies/README.md create mode 100644 samples/calibration-dispatcher-agent/policies/Routing_Guidelines_Document.pdf create mode 100644 samples/calibration-dispatcher-agent/policies/Service_Procedures_Document.pdf create mode 100644 samples/calibration-dispatcher-agent/requirements.txt diff --git a/samples/calibration-dispatcher-agent/.DS_Store b/samples/calibration-dispatcher-agent/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..6f97b2e3c2a9caedfacce3f476ad39de6959a895 GIT binary patch literal 6148 zcmeHKyG{c!5FC>$PNGSf(!anTSW)-_et?jpKsxD=0{vBdS3Zr|N0H=&l!gXorS;hB z9b2B__7*_dehv@78bD7s#Fq_Avwic0-DSkE*crp|FuqLF$i=N@+ztcw_`v?0-{Tz< zM!exbf5q{9^VZXqCk3Q{6p#W^Knh$dkY&2RzuwSV3P^!}uYi3YD&4Rp&Vl~v;NUF) zv18bcbMGaH)dIwpI0rI9D?ud&wPcG?g3fr&yp}iz1|7w3<~ezD$quFX?TlAXM`?i? zrGON;RA3d$wblPe`Vam8B}pqOAO-%E0;StM?Y4YU*4D-2tkzcgGu?7dbTiJK!okZi j(aSLxR*pBG6nWL=+;53 Data Service > Entities > [Entity Name] > Details** + +2. **Context Grounding Index** (Required) + +```python +# In config.py or .env +CONTEXT_GROUNDING_INDEX_NAME="Calibration Procedures" +``` + +Create this index in: **Orchestrator > Tenant > Indexes** + +3. **Folder Path** (Required) + +```python +# In config.py or .env +UIPATH_FOLDER_PATH="Calibration Services" +``` + +## Setup Guide + +- Data Fabric entities and sample data +- Orchestrator Storage Buckets for Context Grounding +- Index creation and management +- Google Maps API configuration +- Action Center application deployment +- MCP Server integration (optional) + +## Running the Agent + +### Production Mode + +```bash +# With full UiPath infrastructure +python3 main.py +``` + +Expected workflow: +1. Analyzes equipment status from Data Fabric +2. Groups devices by city and priority +3. Retrieves routing constraints from Context Grounding +4. Generates optimized routes with Google Maps +5. Presents routes for approval in Action Center +6. Executes RPA workflows (email, Slack, Data Fabric updates) + +### Mock Mode (Local Testing) + +For quick testing without full UiPath setup: + +```python +# In config.py or .env +USE_MOCK_DATA=true +AUTO_APPROVE_IN_LOCAL=true +USE_MCP=false +``` + +**Note**: Mock mode relaxes configuration validation and skips Action Center/MCP integration, but still requires Data Fabric with imported CSV data (see Setup section). + +Then run: + +```bash +python3 main.py +``` + +## Project Structure + +``` +calibration-dispatcher-agent/ +│ +├── 📄 Core Application Files +│ ├── main.py # Main agent logic (LangGraph workflow) +│ ├── config.py # Centralized configuration +│ ├── mcp_bridge.py # Async-to-sync MCP tool bridge +│ ├── requirements.txt # Python dependencies +│ ├── .env.example # Environment variables template +│ ├── .gitignore # Git exclusions +│ └── README.md # This file +│ +├── 📁 data/ # Sample data and schema +│ ├── README.md # Data directory documentation +│ ├── Schema.json # Data Fabric entity definitions +│ ├── devices_for_data_fabric.csv # Sample equipment (20 devices) +│ ├── locations.csv # Sample clinics (20 locations) +│ └── technicians.csv # Sample technicians (5 techs) +│ +│ +└── 📁 policies/ # Policy documents for Context Grounding + ├── README.md # Policies documentation + ├── Calibration_Rules_Document.pdf # Rules, intervals, SLAs + ├── Routing_Guidelines_Document.pdf # Route optimization + └── Service_Procedures_Document.pdf # Service procedures +``` + +## Business Logic + +### Priority Classification + +Devices are classified based on days until next calibration due: + +| Status | Audiometer | Tympanometer | Priority | Action | +|--------|-----------|--------------|----------|---------| +| **OVERDUE** | Past due | Past due | Critical | Immediate scheduling | +| **URGENT** | ≤ 14 days | ≤ 7 days | High | Schedule within 48h | +| **SCHEDULED** | > 14 days | > 7 days | Normal | Regular scheduling | + +### SLA Requirements + +Response times based on clinic classification: + +| Clinic Type | SLA | Example | +|------------|-----|---------| +| **Hospital** | 24 hours | Regional hospitals | +| **Specialist Clinic** | 48 hours | Audiology centers | +| **General Practice** | 72 hours | Family clinics | + +### Routing Constraints + +**Standard Mode:** +- Max 4 visits per route +- Max 200 km total distance +- Max 8 hours total work time + +**OVERDUE Override (Emergency Mode):** +- Max 5 visits per route +- Max 300 km total distance +- Max 12 hours total work time (includes overtime) + +Constraints are retrieved from Context Grounding policies and can be overridden by manager notes. + +### Technician Specialization + +Devices are matched to technicians based on specializations: + +| Device Type | Required Specialization | +|------------|------------------------| +| Audiometer | Audiometry or All | +| Tympanometer | Tympanometry or All | + +## Extending the Sample + +### Adding New Device Types + +1. Update `devices_for_data_fabric.csv` with new device records +2. Add specialization mapping in `config.py`: + ```python + DEVICE_TO_SPECIALIZATION = { + "Audiometer": {"Audiometry", "All"}, + "Tympanometer": {"Tympanometry", "All"}, + "Spirometer": {"Respiratory", "All"}, # New device type + } + ``` +3. Add service time estimation in `config.py`: + ```python + SERVICE_TIME_SPIROMETER = float(os.getenv("SERVICE_TIME_SPIROMETER", "1.0")) + ``` + +### Adding New Cities + +1. Update `locations.csv` with clinic records in the new city +2. Add city coordinates in `config.py`: + ```python + CITY_COORDS = { + "Warsaw": (52.2297, 21.0122), + "Poznan": (52.4064, 16.9252), + "Lodz": (51.7592, 19.4560), # New city + } + ``` + +### Creating Custom Tools + +Add new LangChain tools to extend agent capabilities: + +```python +@tool +def check_parts_availability(device_type: str) -> dict: + """Check if spare parts are available for device calibration.""" + # Your implementation + return {"available": True, "lead_time_days": 2} +``` + +Then include in the agent's tool list. + +## Troubleshooting + +### Common Issues + +**Configuration Validation Errors** + +If you see "Configuration Errors" when running the agent: +- Verify entity IDs are correct (not placeholder `00000000-...`) +- Check that Context Grounding index exists +- Confirm UiPath authentication is valid + +**Context Grounding Not Found** + +If policy retrieval fails: +- Verify index name matches configuration +- Check that storage bucket contains policy PDFs +- Ensure index has been created and synchronized +- Confirm folder permissions allow access + +**Google Maps API Errors** + +If route optimization fails: +- Verify API key is valid and active +- Check that Distance Matrix API is enabled +- Ensure billing is configured in Google Cloud Console +- Routes will fall back to straight-line distance if API unavailable + +**Action Center Task Not Created** + +If HITL approval doesn't work: +- Verify Action Center application is deployed +- Check field names match configuration (`SelectedOutcome`, `ManagerComments`) +- Ensure user has permissions to create tasks +- Try `AUTO_APPROVE_IN_LOCAL=true` for local testing + + +## Support + +For issues or questions: +- Review UiPath SDK documentation +- Contact your UiPath representative + +## Acknowledgments + +This sample demonstrates patterns from the UiPath Specialist Coded Agent Challenge 2025 (4th place solution). diff --git a/samples/calibration-dispatcher-agent/config.py b/samples/calibration-dispatcher-agent/config.py new file mode 100644 index 00000000..6f0d53c3 --- /dev/null +++ b/samples/calibration-dispatcher-agent/config.py @@ -0,0 +1,216 @@ +""" +Calibration Dispatcher Agent - Configuration + +This file contains all environment-specific configurations including: +- UiPath Data Fabric entity IDs +- Folder paths and index names +- LLM model selection +- API keys and service endpoints +- MCP server configuration +- Business logic parameters + +Adjust these values according to your UiPath environment. +""" + +import os +from typing import Dict, Tuple + +# ============================================================================= +# UIPATH PLATFORM CONFIGURATION +# ============================================================================= + +# Folder path in UiPath Orchestrator (where processes and entities are deployed) +UIPATH_FOLDER_PATH = os.getenv("UIPATH_FOLDER_PATH", "Calibration Services") + +# Context Grounding index name (created from Storage Bucket containing calibration policies) +CONTEXT_GROUNDING_INDEX_NAME = os.getenv( + "CONTEXT_GROUNDING_INDEX_NAME", + "Calibration Procedures" +) + +# Number of policy documents to retrieve for RAG +CONTEXT_GROUNDING_NUM_RESULTS = int(os.getenv("CONTEXT_GROUNDING_NUM_RESULTS", "3")) + +# ============================================================================= +# DATA FABRIC ENTITY IDS +# ============================================================================= +# These IDs must match your Data Fabric entities in UiPath Orchestrator. +# You can find them in Data Service > Entities > Entity Details + +EQUIPMENT_ENTITY_ID = os.getenv( + "EQUIPMENT_ENTITY_ID", + "00000000-0000-0000-0000-000000000001" # Replace with your Equipment entity ID +) + +CLINICS_ENTITY_ID = os.getenv( + "CLINICS_ENTITY_ID", + "00000000-0000-0000-0000-000000000002" # Replace with your Clinics entity ID +) + +TECHNICIANS_ENTITY_ID = os.getenv( + "TECHNICIANS_ENTITY_ID", + "00000000-0000-0000-0000-000000000003" # Replace with your Technicians entity ID +) + +# ============================================================================= +# LLM CONFIGURATION +# ============================================================================= + +# Model selection for UiPath LLM Gateway +# Options: "gpt-4o-2024-11-20", "gpt-4o-mini", "claude-sonnet-4-5-20250929", etc. +LLM_MODEL = os.getenv("LLM_MODEL", "gpt-4o-2024-11-20") + +# Temperature setting for LLM responses (0.0 = deterministic, 1.0 = creative) +LLM_TEMPERATURE = float(os.getenv("LLM_TEMPERATURE", "0.0")) + +# ============================================================================= +# GOOGLE MAPS API CONFIGURATION +# ============================================================================= + +# Google Maps API key for route optimization +# Can be set via environment variable or UiPath Asset +GOOGLE_MAPS_API_KEY = os.getenv("GOOGLE_MAPS_API_KEY", "") + +# UiPath Asset name for Google Maps API key (fallback if env var not set) +GOOGLE_MAPS_ASSET_NAME = os.getenv("GOOGLE_MAPS_ASSET_NAME", "GoogleMapsApiKey") + +# ============================================================================= +# MCP SERVER CONFIGURATION +# ============================================================================= + +# Enable/disable MCP integration (set to "false" to use classic RPA invocation) +USE_MCP = os.getenv("USE_MCP", "true").lower() == "true" + +# MCP server URL from UiPath Orchestrator (MCP Servers page) +MCP_SERVER_URL = os.getenv("MCP_SERVER_URL", "") + +# MCP tool input argument names (must match your RPA workflow parameter names) +MCP_ARG_EMAIL = os.getenv("RPA_ARG_NAME_EMAIL", "in_RouteData") +MCP_ARG_SLACK = os.getenv("RPA_ARG_NAME_SLACK", "in_MessageData") +MCP_ARG_ENTITY = os.getenv("RPA_ARG_NAME_ENTITY", "in_ServiceOrderData") + +# ============================================================================= +# ACTION CENTER CONFIGURATION +# ============================================================================= + +# Action Center form field names (must match your UiPath Apps form design) +APP_FIELD_SELECTED_OUTCOME = os.getenv("APP_FIELD_SELECTED_OUTCOME", "SelectedOutcome") +APP_FIELD_MANAGER_COMMENTS = os.getenv("APP_FIELD_MANAGER_COMMENTS", "ManagerComments") + +# Email address for approval notifications +APPROVER_EMAIL = os.getenv("APPROVER_EMAIL", "manager@example.com") + +# Maximum revision iterations per route before automatic rejection +MAX_REVISION_ITERATIONS = int(os.getenv("MAX_REVISION_ITERATIONS", "3")) + +# ============================================================================= +# BUSINESS LOGIC PARAMETERS +# ============================================================================= + +# City coordinates for distance calculations (lat, lng) +CITY_COORDS: Dict[str, Tuple[float, float]] = { + "Warsaw": (52.2297, 21.0122), + "Poznan": (52.4064, 16.9252), + "Wroclaw": (51.1079, 17.0385), + "Szczecin": (53.4285, 14.5528), + "Krakow": (50.0647, 19.9450), + "Gdansk": (54.3520, 18.6466), +} + +# Device type to technician specialization mapping +DEVICE_TO_SPECIALIZATION: Dict[str, set] = { + "Audiometer": {"Audiometry", "All"}, + "Tympanometer": {"Tympanometry", "All"}, +} + +# Standard service time per device type (hours) +SERVICE_TIME_AUDIOMETER = float(os.getenv("SERVICE_TIME_AUDIOMETER", "2.0")) +SERVICE_TIME_TYMPANOMETER = float(os.getenv("SERVICE_TIME_TYMPANOMETER", "1.5")) + +# Default routing constraints (can be overridden by manager notes) +DEFAULT_MAX_VISITS_PER_ROUTE = int(os.getenv("DEFAULT_MAX_VISITS_PER_ROUTE", "4")) +DEFAULT_MAX_DISTANCE_KM = float(os.getenv("DEFAULT_MAX_DISTANCE_KM", "200.0")) +DEFAULT_MAX_WORK_HOURS = float(os.getenv("DEFAULT_MAX_WORK_HOURS", "8.0")) + +# Override constraints for OVERDUE devices (emergency mode) +OVERDUE_MAX_VISITS_PER_ROUTE = int(os.getenv("OVERDUE_MAX_VISITS_PER_ROUTE", "5")) +OVERDUE_MAX_DISTANCE_KM = float(os.getenv("OVERDUE_MAX_DISTANCE_KM", "300.0")) +OVERDUE_MAX_WORK_HOURS = float(os.getenv("OVERDUE_MAX_WORK_HOURS", "12.0")) + +# Cost parameters for route optimization +COST_PER_KM = float(os.getenv("COST_PER_KM", "0.50")) # EUR per kilometer +TECHNICIAN_HOURLY_RATE = float(os.getenv("TECHNICIAN_HOURLY_RATE", "45.0")) # EUR per hour + +# ============================================================================= +# LOGGING CONFIGURATION +# ============================================================================= + +# Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL +LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper() + +# Log format +LOG_FORMAT = os.getenv( + "LOG_FORMAT", + "%(asctime)s - %(levelname)s - %(name)s - %(message)s" +) + +# ============================================================================= +# MOCK DATA CONFIGURATION (for testing without full UiPath setup) +# ============================================================================= + +# Enable mock mode for local testing (relaxes config validation, requires Data Fabric with imported data) +USE_MOCK_DATA = os.getenv("USE_MOCK_DATA", "true").lower() == "true" + +# Auto-approve all routes in local testing (skips Action Center, auto-approves routes) +AUTO_APPROVE_IN_LOCAL = os.getenv("AUTO_APPROVE_IN_LOCAL", "true").lower() == "true" + +# ============================================================================= +# HELPER FUNCTIONS +# ============================================================================= + +def validate_config() -> bool: + """ + Validate critical configuration values. + Returns True if configuration is valid, False otherwise. + """ + errors = [] + + if not USE_MOCK_DATA: + if EQUIPMENT_ENTITY_ID.startswith("00000000"): + errors.append("EQUIPMENT_ENTITY_ID must be set to your actual Data Fabric entity ID") + + if CLINICS_ENTITY_ID.startswith("00000000"): + errors.append("CLINICS_ENTITY_ID must be set to your actual Data Fabric entity ID") + + if TECHNICIANS_ENTITY_ID.startswith("00000000"): + errors.append("TECHNICIANS_ENTITY_ID must be set to your actual Data Fabric entity ID") + + if USE_MCP and not MCP_SERVER_URL: + errors.append("MCP_SERVER_URL must be set when USE_MCP=true") + + if errors: + print("\n⚠️ Configuration Errors:") + for error in errors: + print(f" - {error}") + print("\n💡 Please update config.py or set environment variables.\n") + return False + + return True + + +def print_config_summary(): + """Print a summary of current configuration (useful for debugging).""" + print("=" * 70) + print("CALIBRATION DISPATCHER AGENT - CONFIGURATION SUMMARY") + print("=" * 70) + print(f"Folder Path: {UIPATH_FOLDER_PATH}") + print(f"Context Grounding: {CONTEXT_GROUNDING_INDEX_NAME}") + print(f"LLM Model: {LLM_MODEL}") + print(f"Google Maps: {'Enabled' if GOOGLE_MAPS_API_KEY else 'Disabled'}") + print(f"MCP Integration: {'Enabled' if USE_MCP else 'Disabled'}") + print(f"Mock Data Mode: {'Enabled' if USE_MOCK_DATA else 'Disabled'}") + print(f"Max Visits/Route: {DEFAULT_MAX_VISITS_PER_ROUTE}") + print(f"Max Distance (km): {DEFAULT_MAX_DISTANCE_KM}") + print(f"Max Work Hours: {DEFAULT_MAX_WORK_HOURS}") + print("=" * 70) + print() diff --git a/samples/calibration-dispatcher-agent/data/README.md b/samples/calibration-dispatcher-agent/data/README.md new file mode 100644 index 00000000..8f37ab3f --- /dev/null +++ b/samples/calibration-dispatcher-agent/data/README.md @@ -0,0 +1,59 @@ +# Data Files + +This directory contains sample data files for the Calibration Dispatcher Agent. + +## Files + +### Schema.json +Data Fabric entity definitions for the four entities used by the agent: +- **Equipment**: Medical devices requiring calibration +- **Clinics**: Healthcare facilities where devices are located +- **Technicians**: Field service technicians who perform calibrations +- **ServiceOrders**: Scheduled calibration visits (created by agent) + +**Usage**: Import this schema into UiPath Orchestrator Data Service to create the required entities. + +### CSV Files + +Sample data for testing and demonstration: + +- **devices_for_data_fabric.csv** (20 records) + - Medical devices (Audiometers and Tympanometers) + - Includes calibration due dates, priorities, and clinic assignments + +- **locations.csv** (20 records) + - Healthcare facilities across 4 Polish cities + - Includes addresses, coordinates, SLA tiers (24h/48h/72h) + - Fictitious contact information (names and emails) + +- **technicians.csv** (5 records) + - Field service technicians with specializations + - Home base cities for route optimization + - Fictitious names and contact information + +## Data Privacy + +All data has been anonymized: +- ✅ Clinic names are generic (e.g., "Regional Hospital No 1") +- ✅ Contact names are common English names +- ✅ Email addresses use `.example` domain +- ✅ Geographic data (cities, coordinates) is real public information + +## Usage + +### For Mock Mode Testing +The agent loads CSV files directly when `USE_MOCK_DATA=true` in config.py. No additional setup needed. + +### For Production Deployment +Import data into Data Fabric: +- manually via Orchestrator UI: **Data Service > Entities > Import** + +## Customization + +Feel free to modify the CSV files to: +- Add your own cities and clinic locations +- Adjust device counts and types +- Change technician assignments +- Modify SLA tiers based on your business needs + +Just maintain the CSV column structure to ensure compatibility. diff --git a/samples/calibration-dispatcher-agent/data/Schema.json b/samples/calibration-dispatcher-agent/data/Schema.json new file mode 100644 index 00000000..3e304d4d --- /dev/null +++ b/samples/calibration-dispatcher-agent/data/Schema.json @@ -0,0 +1,1002 @@ +{ + "entities": [ + { + "name": "Clinics", + "displayName": "Clinics", + "entityTypeId": 0, + "entityType": "Entity", + "description": "Entity to store information about clinics", + "folderId": "00000000-0000-0000-0000-000000000000", + "fields": [ + { + "name": "clinicId", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": true, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Clinic ID", + "description": "A unique identifier for the clinic", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "clinicName", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Clinic Name", + "description": "Name of the clinic", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "address", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Address", + "description": "Address of the clinic", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "city", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "City", + "description": "City of the clinic", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "postalCode", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Postal Code", + "description": "Postal code of the clinic", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "latitude", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "DECIMAL", + "lengthLimit": 1000, + "maxValue": 1000000000000, + "minValue": -1000000000000, + "decimalPrecision": 2 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Latitude", + "description": "Latitude of the clinic location", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "longitude", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "DECIMAL", + "lengthLimit": 1000, + "maxValue": 1000000000000, + "minValue": -1000000000000, + "decimalPrecision": 2 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Longitude", + "description": "Longitude of the clinic location", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "contactPerson", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Contact Person", + "description": "Contact person for the clinic", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "contactEmail", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Contact Email", + "description": "Contact email address for the clinic", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "contactPhone", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": false, + "isEncrypted": false, + "displayName": "Contact Phone", + "description": "Contact phone number for the clinic", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "slaHours", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "DECIMAL", + "lengthLimit": 1000, + "maxValue": 1000000000000, + "minValue": -1000000000000, + "decimalPrecision": 2 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "SLA Hours", + "description": "Service level agreement hours", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + } + ], + "isRbacEnabled": false, + "invalidIdentifiers": [], + "isModelReserved": false + }, + { + "name": "Equipment", + "displayName": "Equipment", + "entityTypeId": 0, + "entityType": "Entity", + "description": "Entity to manage equipment details including calibration and manufacturer information.", + "folderId": "00000000-0000-0000-0000-000000000000", + "fields": [ + { + "name": "equipmentId", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": true, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Equipment ID", + "description": "Unique identifier for the equipment", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "deviceName", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Device Name", + "description": "Name of the device", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "deviceType", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Device Type", + "description": "Type of device", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "serialNumber", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": true, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Serial Number", + "description": "Unique serial number of the equipment", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "clinicId", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Clinic ID", + "description": "Identifier for the clinic associated with the equipment", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "lastCalibrationDate", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "DATETIMEOFFSET", + "lengthLimit": 1000 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Last Calibration Date", + "description": "Date when the equipment was last calibrated", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "calibrationIntervalDays", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Calibration Interval (Days)", + "description": "Interval in days for the calibration", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "nextCalibrationDue", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "DATETIMEOFFSET", + "lengthLimit": 1000 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Next Calibration Due", + "description": "Date when the next calibration is due", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "priority", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Priority", + "description": "Priority level of the equipment", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "manufacturer", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": false, + "isEncrypted": false, + "displayName": "Manufacturer", + "description": "Manufacturer of the equipment", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + } + ], + "isRbacEnabled": false, + "invalidIdentifiers": [], + "isModelReserved": false + }, + { + "name": "ServiceOrders", + "displayName": "Service Orders", + "entityTypeId": 0, + "entityType": "Entity", + "description": "Entity to manage service orders with all necessary local fields.", + "folderId": "00000000-0000-0000-0000-000000000000", + "fields": [ + { + "name": "orderId", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": true, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Order ID", + "description": "", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "equipmentId", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Equipment ID", + "description": "", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "clinicId", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Clinic ID", + "description": "", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "technicianId", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Technician ID", + "description": "", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "scheduledDate", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "DATETIMEOFFSET", + "lengthLimit": 1000 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Scheduled Date", + "description": "", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "estimatedDurationHours", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "DECIMAL", + "lengthLimit": 1000, + "maxValue": 1000000000000, + "minValue": -1000000000000, + "decimalPrecision": 2 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Estimated Duration Hours", + "description": "", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "routeSequence", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "DECIMAL", + "lengthLimit": 1000, + "maxValue": 1000000000000, + "minValue": -1000000000000, + "decimalPrecision": 2 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Route Sequence", + "description": "", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "status", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Status", + "description": "", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "priority", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "DECIMAL", + "lengthLimit": 1000, + "maxValue": 1000000000000, + "minValue": -1000000000000, + "decimalPrecision": 2 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Priority", + "description": "", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "createdDate", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "DATETIMEOFFSET", + "lengthLimit": 1000 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Created Date", + "description": "", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "approvedBy", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": false, + "isEncrypted": false, + "displayName": "Approved By", + "description": "", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "completionDate", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "DATETIMEOFFSET", + "lengthLimit": 1000 + }, + "isRequired": false, + "isEncrypted": false, + "displayName": "Completion Date", + "description": "", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "notes", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": false, + "isEncrypted": false, + "displayName": "Notes", + "description": "", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "routeMapUrl", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": false, + "isEncrypted": false, + "displayName": "Route Map URL", + "description": "", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "totalDistanceKm", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "DECIMAL", + "lengthLimit": 1000, + "maxValue": 1000000000000, + "minValue": -1000000000000, + "decimalPrecision": 2 + }, + "isRequired": false, + "isEncrypted": false, + "displayName": "Total Distance (Km)", + "description": "", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + } + ], + "isRbacEnabled": false, + "invalidIdentifiers": [], + "isModelReserved": false + }, + { + "name": "Technicians", + "displayName": "Technicians", + "entityTypeId": 0, + "entityType": "Entity", + "description": "Entity to manage details of technicians.", + "folderId": "00000000-0000-0000-0000-000000000000", + "fields": [ + { + "name": "technicianId", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": true, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Technician ID", + "description": "A unique identifier for the technician", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "technicianName", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Technician Name", + "description": "Name of the technician", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "email", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": true, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Email", + "description": "Email address of the technician", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "phone", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": false, + "isEncrypted": false, + "displayName": "Phone", + "description": "Phone number of the technician", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "specialization", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Specialization", + "description": "Specialization area of the technician", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + }, + { + "name": "homeBaseCity", + "isPrimaryKey": false, + "isForeignKey": false, + "isExternalField": false, + "isHiddenField": false, + "fieldCategoryId": 0, + "isUnique": false, + "referenceType": "ManyToOne", + "sqlType": { + "name": "NVARCHAR", + "lengthLimit": 200 + }, + "isRequired": true, + "isEncrypted": false, + "displayName": "Home Base City", + "description": "Home base city of the technician", + "isSystemField": false, + "isAttachment": false, + "isRbacEnabled": false, + "isModelReserved": false + } + ], + "isRbacEnabled": false, + "invalidIdentifiers": [], + "isModelReserved": false + } + ], + "choicesets": [] +} \ No newline at end of file diff --git a/samples/calibration-dispatcher-agent/data/devices_for_data_fabric.csv b/samples/calibration-dispatcher-agent/data/devices_for_data_fabric.csv new file mode 100644 index 00000000..612f077d --- /dev/null +++ b/samples/calibration-dispatcher-agent/data/devices_for_data_fabric.csv @@ -0,0 +1,21 @@ +Equipment ID,Device Name,Device Type,Serial Number,Clinic ID,Last Calibration Date,Calibration Interval (Days),Next Calibration Due,Priority,Manufacturer +EQP001,AudioStar Pro 3000,Audiometer,AS3K-2024-001,CLI001,2024-11-01,365,2025-11-01,2,MedTech Solutions +EQP002,HearTest Elite,Audiometer,HTE-2024-042,CLI002,2024-10-20,365,2025-10-20,1,AudioCorp +EQP003,TympScan 500,Tympanometer,TS500-2024-118,CLI003,2024-11-05,365,2025-11-05,2,MedTech Solutions +EQP004,AudioMaster Plus,Audiometer,AMP-2024-089,CLI004,2024-10-18,365,2025-10-18,1,HearingTech +EQP005,PureTone Pro,Audiometer,PTP-2024-156,CLI005,2024-12-15,365,2025-12-15,4,AudioCorp +EQP006,TympCheck Advanced,Tympanometer,TCA-2024-201,CLI006,2024-10-25,365,2025-10-25,1,DiagnosticSys +EQP007,HearingTest 2000,Audiometer,HT2K-2024-067,CLI007,2024-11-10,365,2025-11-10,2,MedTech Solutions +EQP008,AudioPro X1,Audiometer,APX1-2024-134,CLI008,2025-01-20,365,2026-01-20,5,HearingTech +EQP009,TympMaster Elite,Tympanometer,TME-2024-245,CLI009,2024-10-22,365,2025-10-22,1,AudioCorp +EQP010,PureTone Advanced,Audiometer,PTA-2024-178,CLI010,2024-11-08,365,2025-11-08,2,MedTech Solutions +EQP011,AudioCheck Pro,Audiometer,ACP-2024-092,CLI011,2025-02-05,365,2026-02-05,5,DiagnosticSys +EQP012,HearTest Pro 500,Audiometer,HTP5-2024-115,CLI012,2024-11-14,365,2025-11-14,3,AudioCorp +EQP013,TympScan Ultra,Tympanometer,TSU-2024-267,CLI013,2024-10-28,365,2025-10-28,1,HearingTech +EQP014,AudioMaster 3000,Audiometer,AM3K-2024-203,CLI014,2024-11-12,365,2025-11-12,2,MedTech Solutions +EQP015,HearingPro Elite,Audiometer,HPE-2024-145,CLI015,2025-01-25,365,2026-01-25,5,AudioCorp +EQP016,TympTest Advanced,Tympanometer,TTA-2024-289,CLI016,2024-10-30,365,2025-10-30,1,DiagnosticSys +EQP017,AudioStar Elite,Audiometer,ASE-2024-167,CLI017,2024-11-18,365,2025-11-18,2,MedTech Solutions +EQP018,PureTone Ultra,Audiometer,PTU-2024-189,CLI018,2025-03-10,365,2026-03-10,5,HearingTech +EQP019,TympCheck Pro,Tympanometer,TCP-2024-312,CLI019,2024-11-02,365,2025-11-02,2,AudioCorp +EQP020,AudioTest Advanced,Audiometer,ATA-2024-334,CLI020,2024-11-20,365,2025-11-20,3,MedTech Solutions diff --git a/samples/calibration-dispatcher-agent/data/locations.csv b/samples/calibration-dispatcher-agent/data/locations.csv new file mode 100644 index 00000000..df7a6336 --- /dev/null +++ b/samples/calibration-dispatcher-agent/data/locations.csv @@ -0,0 +1,21 @@ +Clinic ID,Clinic Name,Address,City,Postal Code,Latitude,Longitude,Contact Person,Contact Email,Contact Phone,SLA Hours +CLI001,Regional Hospital No 1,ul. Arkońska 4,Szczecin,71-455,53.4285,14.5528,Anna Kowalski,contact@clinic001.example,+48914343434,24 +CLI002,Health Center North,ul. Grunwaldzka 182,Poznan,60-166,52.4064,16.9252,Tom Nowak,contact@clinic002.example,+48618888888,48 +CLI003,Central Medical Center,al. Jerozolimskie 123,Warsaw,02-017,52.2297,21.0122,Maria Smith,contact@clinic003.example,+48225551234,24 +CLI004,Audio Clinic West,ul. Piłsudskiego 12,Wroclaw,50-044,51.1079,17.0385,Jan Johnson,contact@clinic004.example,+48713334455,48 +CLI005,Children's Hospital,ul. Szpitalna 27,Poznan,60-572,52.4012,16.8856,Kate Brown,contact@clinic005.example,+48617776666,24 +CLI006,Medica Clinic,ul. Monte Cassino 40,Szczecin,70-466,53.4395,14.5481,Peter Davis,contact@clinic006.example,+48914567890,72 +CLI007,Hearing Center Pro,ul. Nowy Świat 33,Warsaw,00-029,52.2350,21.0177,Amy Wilson,contact@clinic007.example,+48226667788,48 +CLI008,ENT Clinic,ul. Świdnicka 50,Wroclaw,50-030,51.1054,17.0262,Mark Miller,contact@clinic008.example,+48719998877,72 +CLI009,University Hospital,ul. Przybyszewskiego 49,Poznan,60-355,52.4199,16.9016,Eve Moore,contact@clinic009.example,+48611112233,24 +CLI010,ProMed Clinic,ul. Ku Słońcu 67,Szczecin,71-080,53.4525,14.5003,Bob Taylor,contact@clinic010.example,+48914445566,72 +CLI011,Diagnostic Center,ul. Pulawska 455,Warsaw,02-844,52.1621,21.0305,Lisa Anderson,contact@clinic011.example,+48223334455,48 +CLI012,ENT Clinic South,ul. Borowska 213,Wroclaw,50-556,51.0826,17.0048,Andy Thomas,contact@clinic012.example,+48717778899,72 +CLI013,Family Clinic,ul. Słowackiego 12,Poznan,60-823,52.4321,16.9104,Jane Jackson,contact@clinic013.example,+48619990011,72 +CLI014,City Hospital,ul. Unii Lubelskiej 1,Szczecin,71-252,53.4150,14.5306,Mark White,contact@clinic014.example,+48911112233,24 +CLI015,Health Center East,ul. Marszałkowska 140,Warsaw,00-061,52.2293,21.0149,Donna Harris,contact@clinic015.example,+48224445566,48 +CLI016,Audio Clinic East,ul. Legnicka 40,Wroclaw,53-671,51.1389,16.9737,Chris Martin,contact@clinic016.example,+48715556677,72 +CLI017,MediCare Clinic,ul. Hetmańska 90,Poznan,60-251,52.4510,16.9342,Maggie Garcia,contact@clinic017.example,+48616667788,48 +CLI018,Clinical Hospital Central,ul. Banacha 1a,Warsaw,02-097,52.2107,20.9826,Greg Martinez,contact@clinic018.example,+48227778899,24 +CLI019,Medical Center North,ul. Niepodległości 30,Szczecin,70-404,53.4308,14.5420,Isabel Rodriguez,contact@clinic019.example,+48918889900,48 +CLI020,ENT Clinic West,ul. Traugutta 57,Wroclaw,50-417,51.1144,17.0211,Adam Lee,contact@clinic020.example,+48719991122,48 diff --git a/samples/calibration-dispatcher-agent/data/technicians.csv b/samples/calibration-dispatcher-agent/data/technicians.csv new file mode 100644 index 00000000..ae6265e6 --- /dev/null +++ b/samples/calibration-dispatcher-agent/data/technicians.csv @@ -0,0 +1,6 @@ +Technician ID,Technician Name,Email,Phone,Specialization,Home Base City +TECH001,John Smith,john.smith@calibration-services.example,+48601111111,All,Warsaw +TECH002,Anna Johnson,anna.johnson@calibration-services.example,+48602222222,Audiometry,Poznan +TECH003,Michael Brown,michael.brown@calibration-services.example,+48603333333,Tympanometry,Wroclaw +TECH004,Sarah Davis,sarah.davis@calibration-services.example,+48604444444,All,Szczecin +TECH005,David Wilson,david.wilson@calibration-services.example,+48605555555,Audiometry,Warsaw diff --git a/samples/calibration-dispatcher-agent/main.py b/samples/calibration-dispatcher-agent/main.py new file mode 100644 index 00000000..039562af --- /dev/null +++ b/samples/calibration-dispatcher-agent/main.py @@ -0,0 +1,1272 @@ +# -*- coding: utf-8 -*- +""" +Calibration Dispatcher Agent - StateGraph + HITL + Constraints Enforcement + +A production-grade autonomous agent for medical device calibration scheduling. + +Features: +- LangGraph StateGraph workflow with Human-in-the-Loop (HITL) via UiPath Action Center +- Dynamic constraint management with manager override capabilities +- Google Maps API integration for route optimization +- Context Grounding for policy retrieval (RAG pattern) +- MCP Server integration for RPA workflow execution +- Technician specialization matching and SLA-aware scheduling + +For configuration, see config.py +""" + +import json +import math +import uuid +import logging +import re +import os +from datetime import datetime, timedelta, date +from typing import Any, Dict, List, Optional, Tuple, Union + +from dotenv import load_dotenv +from pydantic import BaseModel + +from langchain_core.messages import HumanMessage +from langchain_core.tools import tool +from langchain.agents import create_agent as create_react_agent +from langgraph.graph import StateGraph, END +from langgraph.types import interrupt, Command + +from uipath.platform import UiPath + +from uipath.platform.common import CreateTask +from uipath_langchain.chat import UiPathChat +from uipath_langchain.retrievers import ContextGroundingRetriever + +import googlemaps + +# Import centralized configuration +import config + +# ---------- Bootstrap ---------- + +load_dotenv() + +logging.basicConfig( + level=getattr(logging, config.LOG_LEVEL, logging.INFO), + format=config.LOG_FORMAT +) +logger = logging.getLogger("calibration-dispatcher") + +# Validate configuration before proceeding +if not config.validate_config(): + logger.error("Configuration validation failed. Please check config.py") + if not config.USE_MOCK_DATA: + raise RuntimeError("Invalid configuration. Cannot proceed.") + +config.print_config_summary() + +# Initialize UiPath client +uipath_client = UiPath() + +# Initialize LLM +llm = UiPathChat( + model=config.LLM_MODEL, + temperature=config.LLM_TEMPERATURE +) + +# Initialize Context Grounding for policy retrieval +context_grounding = ContextGroundingRetriever( + index_name=config.CONTEXT_GROUNDING_INDEX_NAME, + folder_path=config.UIPATH_FOLDER_PATH, + number_of_results=config.CONTEXT_GROUNDING_NUM_RESULTS, +) + +# Initialize Google Maps client +GOOGLE_MAPS_API_KEY = config.GOOGLE_MAPS_API_KEY +if not GOOGLE_MAPS_API_KEY: + try: + logger.info("Google Maps API key not in config, trying UiPath Assets...") + asset = uipath_client.assets.retrieve( + name=config.GOOGLE_MAPS_ASSET_NAME, + folder_path=config.UIPATH_FOLDER_PATH + ) + GOOGLE_MAPS_API_KEY = getattr(asset, "value", None) or getattr(asset, "stringValue", None) + if GOOGLE_MAPS_API_KEY: + logger.info("Google Maps API key loaded from Assets.") + else: + logger.warning("Asset '%s' found but value is empty.", config.GOOGLE_MAPS_ASSET_NAME) + except Exception as e: + logger.warning("Failed to retrieve Asset: %s", e) + +if GOOGLE_MAPS_API_KEY: + try: + gmaps = googlemaps.Client(key=GOOGLE_MAPS_API_KEY) + logger.info("Google Maps client initialized successfully.") + except Exception as e: + gmaps = None + logger.error("Failed to initialize Google Maps client: %s", e) +else: + gmaps = None + logger.error("Google Maps client NOT initialized - missing API key!") + +# ---------- Global buffer to avoid double planning ---------- + +LAST_ROUTING_PLAN: Optional[Dict[str, Any]] = None + +# ---------- Helpers: specialization, geometry ---------- + +def _estimate_service_hours_for_visit(visit: Dict[str, Any]) -> float: + """Calculate total service hours for a clinic visit based on device types.""" + s = 0.0 + for d in visit.get("devices", []): + if d.get("device_type") == "Audiometer": + s += config.SERVICE_TIME_AUDIOMETER + elif d.get("device_type") == "Tympanometer": + s += config.SERVICE_TIME_TYMPANOMETER + return s + +def _tech_ok_for_devices(tech: Dict[str, Any], visits: List[Dict[str, Any]]) -> bool: + """Check if technician specialization matches required device types.""" + spec = {tech.get("specialization") or "All"} + required = set() + for v in visits: + for d in v.get("devices", []): + required |= config.DEVICE_TO_SPECIALIZATION.get(d.get("device_type"), {"All"}) + return bool(spec & required) or "All" in spec + +def _city_distance_km(city_a: str, city_b: str) -> float: + """Calculate approximate distance between two cities in kilometers.""" + ax, ay = config.CITY_COORDS.get(city_a, (0.0, 0.0)) + bx, by = config.CITY_COORDS.get(city_b, (0.0, 0.0)) + return math.hypot(ax - bx, ay - by) * 111.0 # ~111 km per degree + +def _pick_technician_for_city(technicians: List[Dict[str, Any]], city: str, visits: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]: + candidates = [t for t in technicians if _tech_ok_for_devices(t, visits)] + local = [t for t in candidates if t.get("home_base_city") == city] + if local: + return local[0] + ranked = sorted(candidates or technicians, key=lambda t: _city_distance_km(t.get("home_base_city") or "", city)) + return ranked[0] if ranked else None + +# ---------- Helpers: policy limits parsing & fallbacks ---------- + +def _extract_json_like(text: str) -> Optional[Dict[str, Any]]: + if not text: + return None + start = text.find("{") + if start == -1: + return None + depth = 0 + for i in range(start, len(text)): + if text[i] == "{": + depth += 1 + elif text[i] == "}": + depth -= 1 + if depth == 0: + snippet = text[start:i+1] + try: + return json.loads(snippet) + except Exception: + return None + return None + +def _parse_manager_note(manager_note: str) -> Dict[str, Any]: + note = (manager_note or "").lower() + out: Dict[str, Any] = {} + m = re.search( r"(?:max(?:imum)?|up\s+to|allow(?:ed)?(?:\s+up\s+to)?|no\s+more\s+than|at\s+most)" + r"\s*([0-9]+(?:\.[0-9]+)?)\s*(?:h|hour(?:s)?)", note) + if m: + out["max_work_hours"] = float(m.group(1)) + m = re.search(r"(?:max\s*)?([0-9]+)\s*(?:visits?|locations?|stops?|sites?)", note) + if m: + out["max_visits_per_route"] = int(m.group(1)) + m = re.search(r"([0-9]+(?:\.[0-9]+)?)\s*km", note) + if m: + out["max_distance_km_per_route"] = float(m.group(1)) + if any(k in note for k in ["overtime", "extra hours", "sla", "weekend", "extend hours", "longer day"]): + out["allow_overtime"] = True + + special_requirements = [] + if any(k in note for k in ["support", "help", "assist", "backup"]): + special_requirements.append("cross_city_support_requested") + if any(k in note for k in ["travel to", "go to", "visit", "send to"]): + for city in ["warszawa", "poznan", "wroclaw", "szczecin", "krakow", "gdansk"]: + if city in note: + special_requirements.append(f"travel_to_{city}") + if any(k in note for k in ["urgent", "asap", "immediately", "priority", "critical"]): + special_requirements.append("urgent_priority") + if any(k in note for k in ["short day", "shorter hours", "reduce hours", "early finish"]): + special_requirements.append("shorter_workday") + if special_requirements: + out["special_requirements"] = special_requirements + out["full_note"] = manager_note + return out + +def _fallback_policy_limits(manager_note: str = "") -> Dict[str, Any]: + """ + Get default routing constraints from config, optionally overridden by manager note. + """ + limits = { + "max_work_hours": config.DEFAULT_MAX_WORK_HOURS, + "max_visits_per_route": config.DEFAULT_MAX_VISITS_PER_ROUTE, + "max_distance_km_per_route": config.DEFAULT_MAX_DISTANCE_KM, + "allow_overtime": False, + } + overrides = _parse_manager_note(manager_note) + limits.update({k: v for k, v in overrides.items() if v is not None}) + logger.warning("Using fallback policy limits: %s", {k: v for k, v in limits.items() if k != "full_note"}) + return limits + +def _derive_policy_limits_via_llm(manager_note: str = "") -> Dict[str, Any]: + """Internal helper: call get_calibration_rules via a tiny ReAct agent and return numeric limits.""" + tools = [get_calibration_rules] + agent_tmp = create_react_agent(llm, tools) + context = "" + if manager_note: + context = ( + "\n\nMANAGER REQUIREMENTS:\n" + f"{manager_note}\n\nIMPORTANT: If manager specifies limits, override default policy values." + ) + msg = ( + "Read corporate policy using get_calibration_rules. Extract numeric limits ONLY as JSON:\n" + '{"max_work_hours": , "max_visits_per_route": , "max_distance_km_per_route": , "allow_overtime": }\n' + "Respond with JSON only, no prose." + context + ) + try: + res = agent_tmp.invoke({"messages": [HumanMessage(content=msg)]}) + raw = res["messages"][-1].content if isinstance(res, dict) else "" + try: + parsed = json.loads(raw) + except Exception: + parsed = _extract_json_like(raw) or {} + if not parsed: + # naive lines + cand: Dict[str, Any] = {} + for line in str(raw).splitlines(): + if ":" not in line: + continue + key, val = [x.strip().strip('",') for x in line.split(":", 1)] + k = key.strip('"').lower().replace(" ", "_") + if k in {"max_work_hours", "max_visits_per_route", "max_distance_km_per_route", "allow_overtime"}: + if k == "allow_overtime": + cand[k] = ("true" in val.lower()) or ("yes" in val.lower()) + elif "visits" in k: + cand[k] = int(re.findall(r"[0-9]+", val)[0]) + else: + cand[k] = float(re.findall(r"[0-9]+(?:\.[0-9]+)?", val)[0]) + parsed = cand + if manager_note: + note_data = _parse_manager_note(manager_note) + for k in ["max_work_hours", "max_visits_per_route", "max_distance_km_per_route", "allow_overtime"]: + if k in note_data and note_data[k] is not None: + parsed[k] = note_data[k] + if "special_requirements" in note_data: + parsed["special_requirements"] = note_data["special_requirements"] + parsed["full_note"] = note_data.get("full_note", manager_note) + return parsed or _fallback_policy_limits(manager_note) + except Exception as e: + logger.warning("Policy limits derivation failed or returned non-JSON: %s", e) + return _fallback_policy_limits(manager_note) + +# ---------- Helpers: weekend SLA date selection ---------- + +def _has_overdue(visits: List[Dict[str, Any]]) -> bool: + for v in visits: + for d in v.get("devices", []): + if d.get("days_until_due", 0) < 0: + return True + return False + +def _choose_route_date(allow_overtime: bool, visits: List[Dict[str, Any]]) -> Tuple[str, bool, str]: + today = date.today() + nxt = today + timedelta(days=1) + is_weekend = nxt.weekday() >= 5 # 5=Sat, 6=Sun + note = "" + if is_weekend: + if nxt.weekday() == 5 and allow_overtime and _has_overdue(visits): + note = "SLA weekend exception applied (Saturday) for OVERDUE devices." + return (nxt.strftime("%Y-%m-%d"), True, note) + delta = 7 - nxt.weekday() + monday = nxt + timedelta(days=delta) + return (monday.strftime("%Y-%m-%d"), False, "Shifted to Monday (no weekend work).") + return (nxt.strftime("%Y-%m-%d"), False, "") + +# ---------- Tools ---------- + +@tool +def analyze_equipment_status() -> Dict[str, Any]: + """LangChain tool: Pull equipment from Data Fabric and split into OVERDUE/URGENT/SCHEDULED/ACTIVE buckets.""" + try: + logger.info("Analyzing equipment status...") + records = uipath_client.entities.list_records(entity_key=config.EQUIPMENT_ENTITY_ID, start=0, limit=100) + today = datetime.now().date() + overdue, urgent, scheduled, active = [], [], [], [] + for record in records: + equipment_id = getattr(record, "equipmentId", None) + device_type = getattr(record, "deviceType", None) + clinic_id = getattr(record, "clinicId", None) + next_due_str = str(getattr(record, "nextCalibrationDue", "")).strip() or None + if not next_due_str or not equipment_id: + continue + try: + next_due = datetime.fromisoformat(next_due_str.split("T")[0].split(" ")[0]).date() + except Exception: + continue + days_until_due = (next_due - today).days + device_info = { + "equipment_id": equipment_id, + "device_type": device_type, + "clinic_id": clinic_id, + "next_due": str(next_due), + "days_until_due": days_until_due, + } + if days_until_due < 0: + overdue.append(device_info) + elif device_type == "Audiometer" and days_until_due <= 14: + urgent.append(device_info) + elif device_type == "Tympanometer" and days_until_due <= 7: + urgent.append(device_info) + elif device_type == "Audiometer" and 15 <= days_until_due <= 30: + scheduled.append(device_info) + elif device_type == "Tympanometer" and 8 <= days_until_due <= 21: + scheduled.append(device_info) + else: + active.append(device_info) + logger.info("Analysis: %d OVERDUE, %d URGENT, %d SCHEDULED", len(overdue), len(urgent), len(scheduled)) + return { + "total_equipment": len(records), + "overdue_count": len(overdue), + "urgent_count": len(urgent), + "scheduled_count": len(scheduled), + "active_count": len(active), + "overdue_devices": overdue[:20], + "urgent_devices": urgent[:20], + "scheduled_devices": scheduled[:20], + "analysis_date": str(today), + } + except Exception as e: + logger.error("Equipment analysis failed: %s", e) + return {"error": str(e)} + +@tool +def get_calibration_rules(query: str) -> str: + """LangChain tool: Retrieve policy fragments from Context Grounding retriever (returns plain text).""" + try: + docs = context_grounding.invoke(query) + if not docs: + return "No specific rules found. Use default thresholds." + rules_text = "\n\n".join([f"Document {i+1}:\n{doc.page_content}" for i, doc in enumerate(docs)]) + logger.info("Retrieved %d rule documents", len(docs)) + return rules_text + except Exception as e: + logger.error("Error retrieving rules: %s", e) + return "Error retrieving rules." + +@tool +def query_clinics() -> List[Dict[str, Any]]: + """LangChain tool: Return list of clinics (id, name, address, city, geo, contacts, SLA).""" + try: + records = uipath_client.entities.list_records(entity_key=config.CLINICS_ENTITY_ID, start=0, limit=100) + clinics_list = [] + for record in records: + clinics_list.append({ + "clinic_id": getattr(record, "clinicId", None), + "clinic_name": getattr(record, "clinicName", None), + "address": getattr(record, "address", None), + "city": getattr(record, "city", None), + "postal_code": getattr(record, "postalCode", None), + "latitude": float(getattr(record, "latitude", "0") or 0), + "longitude": float(getattr(record, "longitude", "0") or 0), + "contact_person": getattr(record, "contactPerson", None), + "contact_email": getattr(record, "contactEmail", None), + "sla_hours": int(getattr(record, "slaHours", 72) or 72), + }) + logger.info("Retrieved %d clinics", len(clinics_list)) + return clinics_list + except Exception as e: + logger.error("Error querying clinics: %s", e) + return [] + +@tool +def query_technicians() -> List[Dict[str, Any]]: + """LangChain tool: Return list of technicians with specialization and home base.""" + try: + records = uipath_client.entities.list_records(entity_key=config.TECHNICIANS_ENTITY_ID, start=0, limit=100) + technicians_list = [] + for record in records: + technicians_list.append({ + "technician_id": getattr(record, "technicianId", None), + "technician_name": getattr(record, "technicianName", None), + "email": getattr(record, "email", None), + "phone": getattr(record, "phone", None), + "specialization": getattr(record, "specialization", None), + "home_base_city": getattr(record, "homeBaseCity", None), + }) + logger.info("Retrieved %d technicians", len(technicians_list)) + return technicians_list + except Exception as e: + logger.error("Error querying technicians: %s", e) + return [] + +@tool +def optimize_route(clinic_ids: List[str], technician_id: Optional[str] = None, city: str = "") -> Dict[str, Any]: + """LangChain tool: Build/optimize a driving route for given clinic IDs; returns km, hours, and a Google Maps URL.""" + if not gmaps: + return {"error": "Google Maps API not configured"} + try: + logger.info("Optimizing route for %d clinics in %s...", len(clinic_ids), city or "?") + all_clinics_records = uipath_client.entities.list_records(entity_key=config.CLINICS_ENTITY_ID, start=0, limit=100) + all_clinics = [{ + "clinic_id": getattr(r, "clinicId", None), + "latitude": float(getattr(r, "latitude", "0") or 0), + "longitude": float(getattr(r, "longitude", "0") or 0), + } for r in all_clinics_records] + clinics = [c for c in all_clinics if c["clinic_id"] in clinic_ids] + if len(clinics) < 1: + return {"error": "Need at least 1 clinic"} + + start_point = None + if technician_id: + tech_records = uipath_client.entities.list_records( + entity_key=config.TECHNICIANS_ENTITY_ID, start=0, limit=100 + ) + for tech in tech_records: + if getattr(tech, "technicianId", None) == technician_id: + home_city = getattr(tech, "homeBaseCity", None) + if home_city and home_city in config.CITY_COORDS: + start_point = config.CITY_COORDS[home_city] + logger.info("Using technician home base: %s", home_city) + break + + if start_point and city in config.CITY_COORDS: + cx, cy = config.CITY_COORDS[city] + dist_km = math.hypot(start_point[0] - cx, start_point[1] - cy) * 111.0 + if dist_km > 80.0: + start_point = (cx, cy) + logger.info("Home base far from cluster; using city centroid for %s as origin", city) + + if start_point: + origin = f"{start_point[0]},{start_point[1]}" + destination = origin + waypoints_coords = [f"{c['latitude']},{c['longitude']}" for c in clinics] + else: + origin = f"{clinics[0]['latitude']},{clinics[0]['longitude']}" + destination = f"{clinics[-1]['latitude']},{clinics[-1]['longitude']}" + waypoints_coords = [f"{c['latitude']},{c['longitude']}" for c in clinics[1:-1]] + + if waypoints_coords: + directions = gmaps.directions( + origin, destination, + waypoints=waypoints_coords, + optimize_waypoints=True, mode="driving" + ) + else: + directions = gmaps.directions(origin, destination, mode="driving") + if not directions: + return {"error": "No route found"} + + route = directions[0] + total_distance_km = sum(leg["distance"]["value"] for leg in route["legs"]) / 1000 + total_duration_hours = sum(leg["duration"]["value"] for leg in route["legs"]) / 3600 + + if waypoints_coords and "waypoint_order" in route: + optimized_order = route["waypoint_order"] + optimized_waypoints = [waypoints_coords[i] for i in optimized_order] + all_waypoints = [origin] + optimized_waypoints + [destination] + logger.info("Using optimized waypoint order: %s", optimized_order) + else: + all_waypoints = [origin] + waypoints_coords + [destination] if waypoints_coords else [origin, destination] + + map_url = "https://www.google.com/maps/dir/" + "/".join(all_waypoints) + logger.info("Route optimized: %.1f km, %.1f h", total_distance_km, total_duration_hours) + return { + "total_distance_km": round(total_distance_km, 1), + "total_duration_hours": round(total_duration_hours, 2), + "starts_from_home": start_point is not None, + "route_map_url": map_url, + } + except Exception as e: + logger.error("Route optimization failed: %s", e) + return {"error": str(e)} + +@tool +def build_routing_plan( + devices_needing_service: Optional[Union[str, List[Dict[str, Any]]]] = None, + max_work_hours: Optional[float] = None, + max_visits_per_route: Optional[int] = None, + max_distance_km_per_route: Optional[float] = None, + allow_overtime: Optional[bool] = None, + manager_note: Optional[str] = None, +) -> Dict[str, Any]: + """LangChain tool: Build an optimized routing plan; enforces hours/visits/distance limits and optional overtime.""" + global LAST_ROUTING_PLAN + try: + if isinstance(devices_needing_service, str): + try: + devices_needing_service = json.loads(devices_needing_service) + logger.info("Parsed devices_needing_service from JSON string") + except Exception as e: + logger.warning("Failed to parse devices_needing_service JSON: %s", e) + devices_needing_service = None + + if not devices_needing_service: + logger.info("No devices provided, fetching from analyze_equipment_status...") + analysis = analyze_equipment_status.invoke({}) + devices_needing_service = analysis.get("overdue_devices", []) + analysis.get("urgent_devices", []) + if not devices_needing_service: + logger.warning("No overdue or urgent devices found") + empty_result = { + "routing_plan": [], + "total_routes": 0, + "total_devices": 0, + "total_distance_km": 0, + } + LAST_ROUTING_PLAN = empty_result + return empty_result + + logger.info("Building routing plan for %d devices...", len(devices_needing_service)) + all_clinics = query_clinics.invoke({}) + all_technicians = query_technicians.invoke({}) + if not all_clinics or not all_technicians: + return {"error": "Missing clinic or technician data"} + + clinic_device_map: Dict[str, Dict[str, Any]] = {} + for device in devices_needing_service: + cid = device["clinic_id"] + if cid not in clinic_device_map: + clinic_info = next((c for c in all_clinics if c["clinic_id"] == cid), None) + if clinic_info: + clinic_device_map[cid] = {"clinic": clinic_info, "devices": []} + if cid in clinic_device_map: + clinic_device_map[cid]["devices"].append(device) + + city_clusters: Dict[str, List[Dict[str, Any]]] = {} + for cid, data in clinic_device_map.items(): + city = data["clinic"]["city"] + city_clusters.setdefault(city, []).append({ + "clinic_id": cid, + "clinic": data["clinic"], + "devices": data["devices"], + }) + logger.info("Created %d city clusters: %s", len(city_clusters), list(city_clusters.keys())) + + routing_plan: List[Dict[str, Any]] = [] + + for city, visits in city_clusters.items(): + logger.info("Processing city %s with %d visits (%d devices total)", + city, len(visits), sum(len(v.get("devices", [])) for v in visits)) + + assigned_tech = _pick_technician_for_city(all_technicians, city, visits) + if not assigned_tech: + continue + + clinic_ids = [v["clinic_id"] for v in visits] + route_result = optimize_route.invoke({ + "clinic_ids": clinic_ids, + "technician_id": assigned_tech["technician_id"], + "city": city, + }) + if "error" in route_result: + logger.warning("Route optimization failed for %s: %s; using fallback", city, route_result["error"]) + n = max(1, len(clinic_ids)) + est_km = n * 8.0 + route_result = { + "total_distance_km": est_km, + "total_duration_hours": round(est_km / 40.0, 2), + "starts_from_home": True, + "route_map_url": "https://www.google.com/maps/search/" + (city or "").replace(" ", "+"), + } + + current_visits = list(visits) + + # Calculate initial work load before expansion + initial_travel = float(route_result.get("total_duration_hours", 0)) + initial_service = sum(_estimate_service_hours_for_visit(x) for x in current_visits) + initial_total = initial_travel + initial_service + + expansion_applied = False + + # EXPANSION: If overtime allowed and there is headroom, try to include all city visits + if allow_overtime and max_work_hours and max_work_hours > 8.0: + hours_available = max_work_hours - initial_total + if hours_available > 1.0: + logger.info("Overtime allowed (%sh). Current: %.2fh, Available: %.2fh. Attempting expansion for %s.", + max_work_hours, initial_total, hours_available, city) + expanded_visits = list(visits) + expanded_ids = [v["clinic_id"] for v in expanded_visits] + expanded_route = optimize_route.invoke({ + "clinic_ids": expanded_ids, + "technician_id": assigned_tech["technician_id"], + "city": city, + }) + expanded_travel = float(expanded_route.get("total_duration_hours", 0)) + expanded_service = sum(_estimate_service_hours_for_visit(x) for x in expanded_visits) + expanded_total = expanded_travel + expanded_service + if expanded_total <= max_work_hours and expanded_total > initial_total + 0.5: + current_visits = expanded_visits + route_result = expanded_route + expansion_applied = True + logger.info("EXPANSION SUCCESS: %d -> %d visits, %.2fh -> %.2fh (limit %sh)", + len(visits), len(current_visits), initial_total, expanded_total, max_work_hours) + elif expanded_total > max_work_hours: + logger.info("EXPANSION FAILED: %d visits would be %.2fh, exceeds %sh limit.", + len(expanded_visits), expanded_total, max_work_hours) + else: + logger.info("EXPANSION SKIPPED: No meaningful improvement (%.2fh -> %.2fh)", + initial_total, expanded_total) + else: + logger.info("EXPANSION SKIPPED: Insufficient headroom (%.2fh used of %sh)", + initial_total, max_work_hours) + + # Apply visit count limit + if max_visits_per_route is not None and len(current_visits) > max_visits_per_route: + current_visits = current_visits[:max_visits_per_route] + + # Apply distance limit + if max_distance_km_per_route is not None and route_result.get("total_distance_km", 0) > max_distance_km_per_route: + while current_visits and route_result.get("total_distance_km", 0) > max_distance_km_per_route: + current_visits = current_visits[:-1] + ids = [v["clinic_id"] for v in current_visits] + if ids: + route_result = optimize_route.invoke({ + "clinic_ids": ids, + "technician_id": assigned_tech["technician_id"], + "city": city, + }) + else: + break + + # Apply hours limit if expansion didn't already validate + if max_work_hours is not None and not expansion_applied: + tmp, chosen = [], [] + for v in current_visits: + tmp.append(v) + ids = [x["clinic_id"] for x in tmp] + rtmp = optimize_route.invoke({ + "clinic_ids": ids, + "technician_id": assigned_tech["technician_id"], + "city": city, + }) + travel_h = float(rtmp.get("total_duration_hours", 0)) + service_h = sum(_estimate_service_hours_for_visit(x) for x in tmp) + if travel_h + service_h <= max_work_hours: + chosen = list(tmp) + else: + break + if chosen: + current_visits = chosen + ids = [v["clinic_id"] for v in current_visits] + route_result = optimize_route.invoke({ + "clinic_ids": ids, + "technician_id": assigned_tech["technician_id"], + "city": city, + }) + + travel_hours = float(route_result.get("total_duration_hours", 0)) + service_hours = sum(_estimate_service_hours_for_visit(x) for x in current_visits) + total_work_hours = travel_hours + service_hours + + planned_date, is_weekend, weekend_note = _choose_route_date(bool(allow_overtime), current_visits) + + routing_plan.append({ + "city": city, + "visits": current_visits, + "technician": assigned_tech, + "route": route_result, + "travel_hours": round(travel_hours, 2), + "service_hours": round(service_hours, 2), + "total_work_hours": round(total_work_hours, 2), + "total_devices": sum(len(v["devices"]) for v in current_visits), + "manager_note": manager_note or "", + "allow_overtime": bool(allow_overtime), + "route_date": planned_date, + "route_date_is_weekend": is_weekend, + "route_date_note": weekend_note, + }) + + logger.info("Created %d routes", len(routing_plan)) + result = { + "routing_plan": routing_plan, + "total_routes": len(routing_plan), + "total_devices": sum(r["total_devices"] for r in routing_plan), + "total_distance_km": sum(r["route"]["total_distance_km"] for r in routing_plan), + } + LAST_ROUTING_PLAN = result + return result + except Exception as e: + logger.error("Routing plan failed: %s", e) + return {"error": str(e)} + +@tool +def request_manager_approval(routing_plan: Dict[str, Any]) -> Dict[str, Any]: + """LangChain tool: Prepare summary for HITL approval (actual Action Center call happens in the HITL node).""" + try: + routes = routing_plan.get("routing_plan", []) + if not routes: + return {"error": "No routes in plan"} + logger.info("Preparing %d approval tasks (will be created in HITL node)...", len(routes)) + task_ids = [str(uuid.uuid4()) for _ in routes] + return { + "task_ids": task_ids, + "total_tasks": len(task_ids), + "action_center_url": "https://cloud.uipath.com/[your-org]/[your-tenant]/actioncenter_/tasks", + "message": f"Prepared {len(task_ids)} approval tasks. Check Action Center.", + } + except Exception as e: + logger.error("Approval request failed: %s", e) + return {"error": str(e)} + +@tool +def create_service_orders(approved_routes: List[Dict[str, Any]]) -> Dict[str, Any]: + """LangChain tool: Create (mock) service orders per approved route; returns counts for reporting.""" + try: + logger.info("Creating service orders for %d routes...", len(approved_routes)) + total_orders = sum(r["total_devices"] for r in approved_routes) + logger.info("Created %d service orders", total_orders) + return { + "total_orders": total_orders, + "orders_per_route": [r["total_devices"] for r in approved_routes], + "message": f"Successfully created {total_orders} service orders in Data Fabric", + } + except Exception as e: + logger.error("Service order creation failed: %s", e) + return {"error": str(e)} + +@tool +def trigger_notification_workflow(service_orders: Dict[str, Any]) -> Dict[str, Any]: + """LangChain tool: Trigger RPA workflow that sends email notifications (delegated to UiPath Orchestrator).""" + try: + total_orders = service_orders.get("total_orders", 0) + logger.info("Triggering notification workflow for %d orders...", total_orders) + return { + "workflow_triggered": True, + "total_notifications": total_orders, + "message": f"RPA workflow will send {total_orders} email notifications", + } + except Exception as e: + logger.error("Notification trigger failed: %s", e) + return {"error": str(e)} + +# ---------- System Prompt ---------- + +SYSTEM_PROMPT = """You are a Calibration Dispatcher Agent for medical equipment routing. + +TASK: Create optimized calibration routes by analyzing equipment status and applying policy constraints. + +EXECUTION STEPS: + +1. ANALYZE EQUIPMENT + Call analyze_equipment_status() to identify OVERDUE and URGENT devices. + OVERDUE: days_until_due < 0 (past calibration deadline) + URGENT: days_until_due <= 14 (Audiometers) or <= 7 (Tympanometers) + +2. RETRIEVE POLICIES + Call get_calibration_rules(query="routing constraints work hours visits distance overtime") + This retrieves company policy documents via Context Grounding. + Extract from policy documents: + - Maximum work hours per technician per day + - Maximum visits per route + - Maximum travel distance per route (km) + - Overtime authorization rules for SLA compliance + +3. BUILD ROUTING PLAN + Call build_routing_plan() with ONLY constraint parameters: + + IMPORTANT: Do NOT pass devices_needing_service parameter - it will be fetched automatically. + + CRITICAL: If manager_note contains explicit constraints (e.g., "max 6 hours"), + use those exact values - manager instructions override all other rules including OVERDUE emergency protocols. + + Example call for OVERDUE devices with no manager constraints: + build_routing_plan( + max_work_hours=12.0, + max_visits_per_route=5, + max_distance_km_per_route=200.0, + allow_overtime=True, + manager_note="" + ) + + Example call when manager specifies "max 6 hours": + build_routing_plan( + max_work_hours=6.0, + max_visits_per_route=4, + max_distance_km_per_route=200.0, + allow_overtime=False, + manager_note="max 6 hours" + ) + + For routes with OVERDUE devices (and no manager constraints): + - Set allow_overtime=True + - Extend max_work_hours to 10-12 hours + - Prioritize immediate service to avoid regulatory violations + + For URGENT-only routes: + - Apply standard policy limits (typically 8 hours, 4 visits, 200km) + - Follow normal scheduling procedures + + Use your reasoning to balance: SLA compliance, technician workload, travel efficiency. + +KEY CONSTRAINTS: +- Manager instructions are ABSOLUTE PRIORITY (override everything) +- Respect technician specialization (Audiometry, Tympanometry, All) +- Minimize total travel distance +- Never exceed daily capacity limits from policy or manager +- OVERDUE devices require immediate action (24-48 hour response) + +OUTPUT: +Provide brief summary: X devices found (Y overdue, Z urgent), constraints applied, N routes created. + +Current date: {current_date} +""" + +# ---------- State / Nodes ---------- + +class WorkflowState(BaseModel): + agent_messages: list = [] + agent_completed: bool = False + routing_plan: Dict[str, Any] = {} + current_route_index: int = 0 + approved_routes: List[Dict[str, Any]] = [] + rejected_routes: List[Dict[str, Any]] = [] + workflow_complete: bool = False + + # Revision tracking for ChangesRequested loop + revision_in_progress: bool = False + current_revision_iteration: int = 0 + pending_manager_note: str = "" + max_revision_iterations: int = config.MAX_REVISION_ITERATIONS + +def _build_agent_comments(route: Dict[str, Any], manager_note: str = "") -> str: + city = route.get("city", "?") + dist = route.get("route", {}).get("total_distance_km", 0.0) + trav = float(route.get("travel_hours", 0.0)) + serv = float(route.get("service_hours", 0.0)) + work = float(route.get("total_work_hours", 0.0)) + tech = route.get("technician", {}) or {} + tech_name = tech.get("technician_name", "N/A") + devices = sum(len(v.get("devices", [])) for v in route.get("visits", [])) + + lines = [ + "AI Agent Analysis:", + f"- Route optimized for minimal travel time ({trav:.2f}h) in {city}", + f"- Technician {tech_name} assigned (specialization matched)", + f"- Total work time: {work:.2f}h (service {serv:.2f}h + travel {trav:.2f}h), distance {dist} km", + f"- {devices} devices scheduled", + ] + if route.get("route_date_is_weekend"): + lines.append(f"- Planned for Saturday due to SLA exception: {route.get('route_date')}") + if route.get("allow_overtime"): + lines.append("- Overtime applied due to SLA-critical OVERDUE devices.") + if manager_note: + lines.append(f"- Manager note applied: {manager_note}") + parsed = _parse_manager_note(manager_note) + if "special_requirements" in parsed: + req_descriptions = { + "cross_city_support_requested": "Cross-city support/assistance identified", + "urgent_priority": "Urgent priority flagged", + } + for req in parsed["special_requirements"]: + if req.startswith("travel_to_"): + city_name = req.replace("travel_to_", "").capitalize() + lines.append(f" * Travel requirement: {city_name}") + elif req in req_descriptions: + lines.append(f" * {req_descriptions[req]}") + lines += [ + "", + "Grounding references:", + "• Service Procedures v4.1 – standard durations (Audiometer 2.0h, Tympanometer 1.5h)", + "• Routing Guidelines v3.2 – daily capacity limits and waypoint optimization", + "• Calibration Rules v2.1 – OVERDUE/URGENT thresholds and SLA windows", + "Decision rationale: minimized distance while staying within documented limits.", + ] + return "\n".join(lines) + +# ---------- helpers ---------- + +def _collect_devices_overdue_and_urgent() -> Tuple[List[Dict[str, Any]], bool]: + analysis = analyze_equipment_status.invoke({}) + overdue = analysis.get("overdue_devices", []) + urgent = analysis.get("urgent_devices", []) + devices = overdue + urgent + has_overdue = len(overdue) > 0 + return devices, has_overdue + +def _compute_limits_for_devices(has_overdue: bool, manager_note: str = "") -> Dict[str, Any]: + """ + Compute routing constraints, with automatic override for OVERDUE devices. + Respects manager explicit instructions when provided. + """ + limits = _derive_policy_limits_via_llm(manager_note) + parsed_note = _parse_manager_note(manager_note) + + manager_set_hours = "max_work_hours" in parsed_note + manager_set_visits = "max_visits_per_route" in parsed_note + manager_wants_shorter = "shorter_workday" in parsed_note.get("special_requirements", []) + + if manager_set_hours or manager_set_visits or manager_wants_shorter: + logger.info("Manager explicit instruction detected, respecting manager limits") + return limits + + if has_overdue: + if not limits.get("allow_overtime"): + logger.info("Auto-enabling overtime for OVERDUE devices (no manager constraint)") + limits["allow_overtime"] = True + if limits.get("max_work_hours", config.DEFAULT_MAX_WORK_HOURS) <= config.DEFAULT_MAX_WORK_HOURS: + limits["max_work_hours"] = config.OVERDUE_MAX_WORK_HOURS + logger.info("Auto-extended to %sh for OVERDUE devices (no manager constraint)", + config.OVERDUE_MAX_WORK_HOURS) + + return limits + +def _plan_with_limits(devices: List[Dict[str, Any]], limits: Dict[str, Any], manager_note: str = "") -> Dict[str, Any]: + return build_routing_plan.invoke({ + "devices_needing_service": devices, + "max_work_hours": limits.get("max_work_hours"), + "max_visits_per_route": limits.get("max_visits_per_route"), + "max_distance_km_per_route": limits.get("max_distance_km_per_route"), + "allow_overtime": limits.get("allow_overtime", False), + "manager_note": manager_note, + }) + +# ---------- Nodes ---------- + +def run_agent_node(state: WorkflowState) -> WorkflowState: + logger.info("=" * 60) + logger.info("PHASE 1: AGENT ANALYSIS & ROUTING") + logger.info("=" * 60) + + current_date = datetime.now().strftime("%Y-%m-%d") + system_prompt = SYSTEM_PROMPT.format(current_date=current_date) + + tools = [analyze_equipment_status, get_calibration_rules, build_routing_plan] + agent_local = create_react_agent(llm, tools) + + user_request = f"""{system_prompt} +Please execute the process above exactly with tools. Then summarize briefly.""" + + def _fallback_plan(path_label: str) -> Dict[str, Any]: + logger.warning("Agent did not produce a routing plan (%s). Falling back to deterministic path.", path_label) + devices, has_overdue = _collect_devices_overdue_and_urgent() + limits = _compute_limits_for_devices(has_overdue) + return _plan_with_limits(devices, limits, manager_note="") + + try: + result = agent_local.invoke({"messages": [HumanMessage(content=user_request)]}) + final_message = result["messages"][-1].content + logger.info("\nAgent response:\n%s\n", final_message) + + routing_plan = LAST_ROUTING_PLAN if LAST_ROUTING_PLAN else _fallback_plan("empty result") + return state.model_copy(update={ + "agent_messages": result["messages"], + "agent_completed": True, + "routing_plan": routing_plan, + }) + except Exception as e: + logger.error("Agent failed: %s", e) + import traceback + logger.error(traceback.format_exc()) + routing_plan = _fallback_plan("exception") + return state.model_copy(update={"agent_completed": True, "routing_plan": routing_plan}) + +def approval_hitl_node(state: WorkflowState) -> Command: + routes = state.routing_plan.get("routing_plan", []) + if state.current_route_index >= len(routes): + return Command(update={}) + + route = routes[state.current_route_index] + + # Local dev mode – skip Action Center and auto-approve the route + if config.AUTO_APPROVE_IN_LOCAL: + logger.info( + "AUTO_APPROVE_IN_LOCAL is enabled - auto-approving route %s (%d/%d)", + route.get("city"), + state.current_route_index + 1, + len(routes), + ) + trigger_rpa_for_route(route) + return Command(update={ + "approved_routes": state.approved_routes + [route], + "current_route_index": state.current_route_index + 1, + "revision_in_progress": False, + "current_revision_iteration": 0, + "pending_manager_note": "", + }) + + if state.revision_in_progress and state.current_revision_iteration > 0: + logger.info("PHASE 2: HITL Approval (%d/%d) - Revision %d for %s", + state.current_route_index + 1, len(routes), + state.current_revision_iteration, route["city"]) + iteration_context = f" (Revision {state.current_revision_iteration}/{state.max_revision_iterations})" + else: + logger.info("PHASE 2: HITL Approval (%d/%d) for %s", + state.current_route_index + 1, len(routes), route["city"]) + iteration_context = "" + + visit_details = "\n".join([ + f"Visit {i+1}: {v['clinic']['clinic_name']} ({len(v['devices'])} devices)" + for i, v in enumerate(route["visits"]) + ]) + + agent_comments = _build_agent_comments( + route, + manager_note=state.pending_manager_note if state.revision_in_progress else "" + ) + + action_data = interrupt(CreateTask( + app_name="Routeapprovalform", + title=f"Route{iteration_context} - {route['city']} - {len(route['visits'])} visits", + data={ + "City": route["city"], + "RouteDate": route.get("route_date"), + "TechnicianName": route["technician"]["technician_name"], + "TotalVisits": len(route["visits"]), + "TotalDistanceKm": route["route"]["total_distance_km"], + "RouteMapUrl": route["route"]["route_map_url"], + "VisitDetails": visit_details, + "TotalServiceHours": route.get("service_hours"), + "TotalTravelHours": route.get("travel_hours"), + "TotalWorkHours": route.get("total_work_hours"), + "AgentComments": agent_comments, + }, + app_version=1, + app_folder_path=config.UIPATH_FOLDER_PATH, + )) + + decision = (action_data.get(config.APP_FIELD_SELECTED_OUTCOME) or "").strip() + manager_note = (action_data.get(config.APP_FIELD_MANAGER_COMMENTS) or "").strip() + if not decision: + logger.warning("SelectedOutcome empty; defaulting to Approved") + decision = "Approved" + + update_dict = {} + + if decision == "Approved": + logger.info("Approved: %s (after %d revision(s))", route["city"], state.current_revision_iteration) + trigger_rpa_for_route(route) + update_dict = { + "approved_routes": state.approved_routes + [route], + "current_route_index": state.current_route_index + 1, + "revision_in_progress": False, + "current_revision_iteration": 0, + "pending_manager_note": "", + } + + elif decision == "Rejected": + logger.info("Rejected: %s", route["city"]) + update_dict = { + "rejected_routes": state.rejected_routes + [route], + "current_route_index": state.current_route_index + 1, + "revision_in_progress": False, + "current_revision_iteration": 0, + "pending_manager_note": "", + } + + elif decision == "ChangesRequested": + next_iteration = state.current_revision_iteration + 1 + + if next_iteration > state.max_revision_iterations: + logger.warning("Max revision iterations (%d) reached for %s. Marking as rejected.", + state.max_revision_iterations, route["city"]) + update_dict = { + "rejected_routes": state.rejected_routes + [route], + "current_route_index": state.current_route_index + 1, + "revision_in_progress": False, + "current_revision_iteration": 0, + "pending_manager_note": "", + } + else: + logger.info("Changes requested (iteration %d/%d). Manager note: %s", + next_iteration, state.max_revision_iterations, manager_note) + + # Get ALL devices in this city + city_name = route["city"] + logger.info("ChangesRequested for %s: fetching all devices in city", city_name) + analysis = analyze_equipment_status.invoke({}) + all_devices = analysis.get("overdue_devices", []) + analysis.get("urgent_devices", []) + all_clinics = query_clinics.invoke({}) + clinics_in_city = [c for c in all_clinics if c.get("city") == city_name] + clinic_ids_in_city = {c["clinic_id"] for c in clinics_in_city} + devices_this_city = [d for d in all_devices if d.get("clinic_id") in clinic_ids_in_city] + + logger.info("Found %d clinics in %s with %d total devices requiring service", + len(clinics_in_city), city_name, len(devices_this_city)) + + limits = _compute_limits_for_devices(has_overdue=len([d for d in devices_this_city if d.get("days_until_due", 1) < 0]) > 0, + manager_note=manager_note) + + revised_plan = _plan_with_limits(devices_this_city, limits, manager_note) + + if revised_plan.get("routing_plan"): + revised_route = revised_plan["routing_plan"][0] + logger.info("Route regenerated successfully for %s", revised_route["city"]) + updated_routes = list(routes) + updated_routes[state.current_route_index] = revised_route + updated_routing_plan = {**state.routing_plan, "routing_plan": updated_routes} + update_dict = { + "routing_plan": updated_routing_plan, + "revision_in_progress": True, + "current_revision_iteration": next_iteration, + "pending_manager_note": manager_note, + } + else: + logger.error("Failed to regenerate route for %s. Marking as rejected.", route["city"]) + update_dict = { + "rejected_routes": state.rejected_routes + [route], + "current_route_index": state.current_route_index + 1, + "revision_in_progress": False, + "current_revision_iteration": 0, + "pending_manager_note": "", + } + + else: + logger.warning("Unknown decision '%s' for %s. Treating as rejected.", decision, route["city"]) + update_dict = { + "rejected_routes": state.rejected_routes + [route], + "current_route_index": state.current_route_index + 1, + "revision_in_progress": False, + "current_revision_iteration": 0, + "pending_manager_note": "", + } + + return Command(update=update_dict) + +def trigger_rpa_for_route(route: Dict[str, Any]) -> bool: + """ + Post-approval side effects for an approved route: + - Email notifications via MCP tool 'Send_Calibration_Notifications' (fallback to classic invoke) + - Slack notification via MCP tool 'Send_Slack_Notification' + - Data Fabric record insert via MCP tool 'AddServiceOrder' + The bridge handles async/sync differences safely. + """ + try: + logger.info("Preparing to trigger post-approval actions for %s", route.get("city")) + # ---------------- Build EMAIL payload (same schema as before) ---------------- + route_data: Dict[str, Any] = { + "City": route.get("city"), + "TechnicianName": route.get("technician", {}).get("technician_name"), + "TechnicianEmail": route.get("technician", {}).get("email"), + "RouteDate": route.get("route_date"), + "TotalVisits": len(route.get("visits", [])), + "TotalDistanceKm": route.get("route", {}).get("total_distance_km"), + "RouteMapUrl": route.get("route", {}).get("route_map_url"), + "Visits": [], + } + for i, visit in enumerate(route.get("visits", []), 1): + visit_data = { + "VisitNumber": i, + "ClinicName": visit.get("clinic", {}).get("clinic_name"), + "ClinicEmail": visit.get("clinic", {}).get("contact_email"), + "ClinicAddress": visit.get("clinic", {}).get("address"), + "Devices": [ + { + "EquipmentId": d.get("equipment_id"), + "DeviceType": d.get("device_type"), + "Status": "OVERDUE" if (d.get("days_until_due", 0) < 0) else "URGENT", + } + for d in visit.get("devices", []) + ], + } + route_data["Visits"].append(visit_data) + + from mcp_bridge import send_calibration_notifications, send_slack_notification, add_service_order + + # ---------------- EMAIL via MCP (or classic fallback) ---------------- + ok_mail = send_calibration_notifications(route_data) + if ok_mail: + logger.info("Email notifications dispatched via MCP/classic.") + else: + logger.error("Email workflow failed via MCP/classic") + + # ---------------- SLACK via MCP ---------------- + clinics_human: List[str] = [] + for v in route.get("visits", []): + c = v.get("clinic", {}) or {} + clinics_human.append(f"{c.get('clinic_name')} - {c.get('address')}") + slack_payload: Dict[str, Any] = { + "technician_name": route.get("technician", {}).get("technician_name"), + "technician_email": route.get("technician", {}).get("email"), + "visit_count": len(route.get("visits", [])), + "city": route.get("city"), + "route_date": route.get("route_date"), + "route_map_url": route.get("route", {}).get("route_map_url"), + "total_distance_km": route.get("route", {}).get("total_distance_km"), + "clinics": clinics_human, + } + ok_slack = send_slack_notification(slack_payload) + if ok_slack: + logger.info("Slack notification sent.") + else: + logger.warning("Slack notification tool returned False (check MCP tool + InArgument).") + + # ---------------- DATA FABRIC via MCP ---------------- + from datetime import datetime as _dt + first_visit = (route.get("visits") or [{}])[0] if route.get("visits") else {} + first_clinic = first_visit.get("clinic", {}) if first_visit else {} + first_device = (first_visit.get("devices") or [{}])[0] if first_visit else {} + technician_id = route.get("technician", {}).get("technician_id") or route.get("technician", {}).get("id") or "TECH-UNKNOWN" + est_hours = route.get("total_work_hours") or (route.get("service_hours", 0.0) or 0.0) + (route.get("travel_hours", 0.0) or 0.0) + order_id = f"ORD-{_dt.now().strftime('%Y%m%d')}-{str(uuid.uuid4())[:6].upper()}" + entity_record: Dict[str, Any] = { + "orderId": order_id, + "clinicId": first_clinic.get("clinic_id") or first_clinic.get("id") or "CLI-UNKNOWN", + "equipmentId": first_device.get("equipment_id") or "EQ-UNKNOWN", + "technicianId": technician_id, + "scheduledDate": route.get("route_date"), + "routeSequence": 1, + "estimatedDurationHours": float(est_hours or 0.0), + "status": "Approved", + "priority": 1, + "notes": "Agent auto-created after manager approval.", + "routeMapUrl": route.get("route", {}).get("route_map_url"), + "totalDistanceKm": route.get("route", {}).get("total_distance_km"), + "approvedBy": config.APPROVER_EMAIL, + "createdDate": _dt.now().astimezone().isoformat(timespec="seconds"), + } + ok_entity = add_service_order(entity_record) + if ok_entity: + logger.info("Service order entity created: %s", entity_record.get("orderId")) + else: + logger.warning("AddServiceOrder tool returned False (check MCP tool + InArgument).") + + # Consider success if at least email went out (Slack & DF are auxiliary) + return bool(ok_mail) + except Exception as e: + logger.error("Post-approval triggers failed for %s: %s", route.get("city"), e, exc_info=True) + return False +def summary_node(state: WorkflowState) -> WorkflowState: + logger.info("=" * 60) + logger.info("WORKFLOW COMPLETE") + logger.info("Approved: %d", len(state.approved_routes)) + logger.info("Rejected: %d", len(state.rejected_routes)) + logger.info("=" * 60) + return state.model_copy(update={"workflow_complete": True}) + +def should_start_approvals(state: WorkflowState) -> str: + routes = state.routing_plan.get("routing_plan", []) + return "approve" if routes else "end" + +def should_continue_approvals(state: WorkflowState) -> str: + if state.revision_in_progress: + logger.info("Revision in progress, looping back to approval for route %d", state.current_route_index + 1) + return "next" + total_routes = len(state.routing_plan.get("routing_plan", [])) + return "next" if state.current_route_index < total_routes else "finish" + +# ---------- Graph ---------- + +graph = StateGraph(WorkflowState) +graph.add_node("agent", run_agent_node) +graph.add_node("approval", approval_hitl_node) +graph.add_node("summary", summary_node) +graph.set_entry_point("agent") + +graph.add_conditional_edges("agent", should_start_approvals, {"approve": "approval", "end": "summary"}) +graph.add_conditional_edges("approval", should_continue_approvals, {"next": "approval", "finish": "summary"}) +graph.add_edge("summary", END) + +agent = graph.compile() + +# ---------- Main ---------- + +if __name__ == "__main__": + logger.info("Starting calibration dispatcher agent...") + initial_state = WorkflowState() + _ = agent.invoke(initial_state) + logger.info("Done!") \ No newline at end of file diff --git a/samples/calibration-dispatcher-agent/mcp_bridge.py b/samples/calibration-dispatcher-agent/mcp_bridge.py new file mode 100644 index 00000000..1c7b6ae6 --- /dev/null +++ b/samples/calibration-dispatcher-agent/mcp_bridge.py @@ -0,0 +1,316 @@ +# mcp_bridge.py +""" +MCP Bridge for UiPath RPA Workflow Integration + +This module provides a safe async-to-sync bridge for calling MCP tools from +LangChain/LangGraph agents. It handles: +- MCP client session management with auto-reconnect +- Async/sync compatibility for LangGraph nodes +- Fallback to classic UiPath process invocation +- Tool discovery and invocation with proper error handling + +For configuration, see config.py +""" +from __future__ import annotations + +import json +import os +import asyncio +import logging +from typing import Dict, Any, Optional, List +from threading import Thread +from concurrent.futures import ThreadPoolExecutor + +# Environment variables +try: + from dotenv import load_dotenv # type: ignore + load_dotenv() +except Exception: + pass + +# Import centralized configuration +import config + +# UiPath SDK +from uipath.platform import UiPath + +# MCP client + LangChain +from mcp import ClientSession +from mcp.client.streamable_http import streamablehttp_client +from langchain_mcp_adapters.tools import load_mcp_tools + +# ---------- Logging ---------- +logging.basicConfig( + level=getattr(logging, config.LOG_LEVEL, logging.INFO), + format=config.LOG_FORMAT +) +log = logging.getLogger("mcp-bridge") + +# ---------- Config ---------- +USE_MCP = config.USE_MCP +MCP_SERVER_URL = config.MCP_SERVER_URL +FOLDER_PATH = config.UIPATH_FOLDER_PATH + +# InArgument names from config +ARG_EMAIL = config.MCP_ARG_EMAIL +ARG_SLACK = config.MCP_ARG_SLACK +ARG_ENTITY = config.MCP_ARG_ENTITY + + +# ---------- UiPath SDK client ---------- +_uipath_client: Optional[UiPath] = None + +def get_uipath_client() -> UiPath: + global _uipath_client + if _uipath_client is None: + _uipath_client = UiPath() + return _uipath_client + +def get_access_token_from_sdk() -> Optional[str]: + try: + client = get_uipath_client() + api_client = getattr(client, "api_client", None) + headers = getattr(api_client, "default_headers", {}) if api_client else {} + auth = headers.get("Authorization", "") + if isinstance(auth, str) and auth.startswith("Bearer "): + token = auth.replace("Bearer ", "", 1) + if token: + return token + except Exception as e: + log.debug("Could not read token from SDK: %s", e) + return None + +def get_access_token() -> Optional[str]: + return os.getenv("UIPATH_ACCESS_TOKEN") or get_access_token_from_sdk() + + +_bg_loop: Optional[asyncio.AbstractEventLoop] = None +_bg_thread: Optional[Thread] = None + +def _ensure_bg_loop(): + global _bg_loop, _bg_thread + if _bg_loop and _bg_loop.is_running(): + return + _bg_loop = asyncio.new_event_loop() + def _runner(): + asyncio.set_event_loop(_bg_loop) # Required for anyio compatibility + _bg_loop.run_forever() + _bg_thread = Thread(target=_runner, name="mcp-bg-loop", daemon=True) + _bg_thread.start() + +def _run_coro_sync(coro): + _ensure_bg_loop() + fut = asyncio.run_coroutine_threadsafe(coro, _bg_loop) # type: ignore[arg-type] + return fut.result() + +# ---------- MCP session ---------- +_http_cm = None # async context manager transportu MCP +_session_read = None +_session_write = None +_transport = None +_session: Optional[ClientSession] = None +_tools_cache: List = [] +_session_lock = asyncio.Lock() + +async def _open_session() -> ClientSession: + """Open new session mcp""" + global _http_cm, _session_read, _session_write, _transport, _session + token = get_access_token() + headers = {"Authorization": f"Bearer {token}"} if token else {} + if not MCP_SERVER_URL: + raise RuntimeError("MCP_SERVER_URL is not set. Provide it from Orchestrator (MCP Servers).") + + + _http_cm = streamablehttp_client(url=MCP_SERVER_URL, headers=headers, timeout=90) + + _session_read, _session_write, _transport = await _http_cm.__aenter__() + try: + sid = _transport.get_session_id() if hasattr(_transport, "get_session_id") else "unknown" + log.info("Received session ID: %s", sid) + except Exception: + log.info("Received session ID: unknown") + + _session = ClientSession(_session_read, _session_write) + await _session.__aenter__() + await _session.initialize() + log.info("Negotiated protocol version: %s", getattr(_session, "protocol_version", "unknown")) + return _session + +async def _ensure_session() -> ClientSession: + global _session + if _session is not None: + return _session + async with _session_lock: + if _session is None: + _session = await _open_session() + return _session + +async def _close_session(): + """Close session MCP""" + global _http_cm, _session_read, _session_write, _transport, _session + try: + if _session is not None: + try: + await _session.__aexit__(None, None, None) + finally: + _session = None + if _http_cm is not None: + try: + await _http_cm.__aexit__(None, None, None) + finally: + _http_cm = None + finally: + _session_read = None + _session_write = None + _transport = None + +# clean shutdown at exit +import atexit +def _shutdown_sync(): + try: + if _bg_loop and _bg_loop.is_running(): + # close mcp session + asyncio.run_coroutine_threadsafe(_close_session(), _bg_loop).result(timeout=3) + _bg_loop.call_soon_threadsafe(_bg_loop.stop) + if _bg_thread: + _bg_thread.join(timeout=2) + except Exception: + pass +atexit.register(_shutdown_sync) + +async def get_mcp_tools(refresh: bool = False): + """Download tools MCP""" + global _tools_cache + if refresh: + _tools_cache = [] + if _tools_cache: + return _tools_cache + async with _session_lock: + if _tools_cache: + return _tools_cache + session = await _ensure_session() + _tools_cache = await load_mcp_tools(session) + names = [t.name for t in _tools_cache] + log.info("MCP tools discovered: %s", names) + return _tools_cache + +def _normalize(s: str) -> str: + return (s or "").lower().replace(" ", "_") + +async def _find_tool(name: str): + tools = await get_mcp_tools() + target = _normalize(name) + for t in tools: + if _normalize(t.name) == target: + return t + for t in tools: + if target in _normalize(t.name): + return t + return None + +async def _invoke_tool_once(tool_name: str, payload: Any, arg_name: str) -> Any: + """ + Single try to invoke MCP tool + """ + await _ensure_session() + tool = await _find_tool(tool_name) + if not tool: + await get_mcp_tools(refresh=True) + tool = await _find_tool(tool_name) + if not tool: + raise RuntimeError( + f"MCP tool '{tool_name}' not found. " + f"Exposed tools: {[t.name for t in await get_mcp_tools()]}" + ) + + payload_str = payload if isinstance(payload, str) else json.dumps(payload, ensure_ascii=False) + args = {arg_name: payload_str} + + log.info("Calling MCP tool '%s' with arg_name='%s', payload_len=%d chars", + tool.name, arg_name, len(payload_str)) + if log.isEnabledFor(logging.DEBUG): + log.debug("Payload preview: %s", payload_str[:500]) + + return await tool.ainvoke(args) + +async def _invoke_tool(tool_name: str, payload: Any, arg_name: str) -> Any: + """ + Invoke tool with auto-reconnect and 1 retry if stream is closed + or "cancel scope/asyncgen" error occurs. + """ + from anyio import ClosedResourceError + try: + return await _invoke_tool_once(tool_name, payload, arg_name) + except (ClosedResourceError, RuntimeError, GeneratorExit) as e: + msg = str(e) + if isinstance(e, ClosedResourceError) or "cancel scope" in msg or "asynchronous generator" in msg: + log.warning("MCP stream/context issue. Reconnecting... (%s)", msg or type(e).__name__) + await _close_session() + await _open_session() + await get_mcp_tools(refresh=True) + return await _invoke_tool_once(tool_name, payload, arg_name) + raise + +# ====================================================================== +# PUBLIC API +# ====================================================================== + +def send_calibration_notifications_mcp(route_data: Dict[str, Any]) -> bool: + async def _run(): + return await _invoke_tool("send_Calibration_Notifications", route_data, ARG_EMAIL) + try: + res = _run_coro_sync(_run()) + log.info("MCP email workflow completed: %s", str(res)[:200]) + return True + except Exception as e: + log.error("MCP email workflow failed: %s", e, exc_info=True) + return False + +def send_slack_notification_mcp(slack_payload: Dict[str, Any]) -> bool: + async def _run(): + return await _invoke_tool("send_Slack_Notification", slack_payload, ARG_SLACK) + try: + res = _run_coro_sync(_run()) + log.info("MCP Slack workflow completed: %s", str(res)[:200]) + return True + except Exception as e: + log.error("MCP Slack workflow failed: %s", e, exc_info=True) + return False + +def add_service_order_mcp(record: Dict[str, Any]) -> bool: + async def _run(): + return await _invoke_tool("addServiceOrder", record, ARG_ENTITY) + try: + res = _run_coro_sync(_run()) + log.info("MCP AddServiceOrder completed: %s", str(res)[:200]) + return True + except Exception as e: + log.error("MCP AddServiceOrder failed: %s", e, exc_info=True) + return False + +# ---------- Classic fallback (e-mail) ---------- +def send_calibration_notifications_classic(route_data: Dict[str, Any]) -> bool: + try: + client = get_uipath_client() + payload = json.dumps(route_data, ensure_ascii=False) + res = client.processes.invoke( + name="Send_Calibration_Notifications", + folder_path=FOLDER_PATH or None, + input_arguments={ARG_EMAIL: payload}, + ) + job_id = getattr(res, "id", None) or str(res) + log.info("Classic invoke OK. Job: %s", job_id) + return True + except Exception as e: + log.error("Classic invoke failed: %s", e, exc_info=True) + return False + +# ---------- Facade used by main ---------- +def send_calibration_notifications(route_data: Dict[str, Any]) -> bool: + return send_calibration_notifications_mcp(route_data) if USE_MCP else send_calibration_notifications_classic(route_data) + +def send_slack_notification(payload: Dict[str, Any]) -> bool: + return send_slack_notification_mcp(payload) if USE_MCP else False + +def add_service_order(record: Dict[str, Any]) -> bool: + return add_service_order_mcp(record) if USE_MCP else False diff --git a/samples/calibration-dispatcher-agent/policies/Calibration_Rules_Document.pdf b/samples/calibration-dispatcher-agent/policies/Calibration_Rules_Document.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ad1b3a862bafe3bfe0d6143cc379b41417c84a15 GIT binary patch literal 309049 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