diff --git a/.cache.sqlite b/.cache.sqlite new file mode 100644 index 0000000..2503e2e Binary files /dev/null and b/.cache.sqlite differ diff --git a/__pycache__/config.cpython-311.pyc b/__pycache__/config.cpython-311.pyc index 62915ce..c9329e2 100644 Binary files a/__pycache__/config.cpython-311.pyc and b/__pycache__/config.cpython-311.pyc differ diff --git a/api/models/__pycache__/requests.cpython-311.pyc b/api/models/__pycache__/requests.cpython-311.pyc index 7adad53..27651ac 100644 Binary files a/api/models/__pycache__/requests.cpython-311.pyc and b/api/models/__pycache__/requests.cpython-311.pyc differ diff --git a/api/models/__pycache__/responses.cpython-311.pyc b/api/models/__pycache__/responses.cpython-311.pyc index 59b035a..1935afe 100644 Binary files a/api/models/__pycache__/responses.cpython-311.pyc and b/api/models/__pycache__/responses.cpython-311.pyc differ diff --git a/api/models/requests.py b/api/models/requests.py index c7bbb31..9f0731f 100644 --- a/api/models/requests.py +++ b/api/models/requests.py @@ -16,5 +16,6 @@ class ChatRequest(BaseModel): - +class SurveyRequest(BaseModel): + job_id: str diff --git a/api/models/responses.py b/api/models/responses.py index 5edff3a..c954386 100644 --- a/api/models/responses.py +++ b/api/models/responses.py @@ -1,8 +1,12 @@ # api/models/responses.py from pydantic import BaseModel - - +from typing import Dict class ChatResponse(BaseModel): status: str message: str + + +class SurveyAgentResponse(BaseModel): + status: str + result: Dict \ No newline at end of file diff --git a/api/routes/__pycache__/chat.cpython-311.pyc b/api/routes/__pycache__/chat.cpython-311.pyc index 4f8a308..9200bca 100644 Binary files a/api/routes/__pycache__/chat.cpython-311.pyc and b/api/routes/__pycache__/chat.cpython-311.pyc differ diff --git a/api/routes/chat.py b/api/routes/chat.py index 0fe5dfd..b5859cf 100644 --- a/api/routes/chat.py +++ b/api/routes/chat.py @@ -1,17 +1,19 @@ # api/routes/chat_ai.py from fastapi import APIRouter, Depends, HTTPException -from api.models.requests import ChatRequest, ChatMessage -from api.models.responses import ChatResponse +from api.models.requests import ChatRequest, ChatMessage,SurveyRequest +from api.models.responses import ChatResponse,SurveyAgentResponse from api.dependencies.auth import get_api_key from src.llm.orchestrator import DroneBot, Message # Adjust import as needed +from src.llm.agent.flight_assesment import DroneAssessmentAgent +import json router = APIRouter( - prefix="/chat-ai", + prefix="/chat", tags=["chat"] ) -@router.post("", response_model=ChatResponse) +@router.post("/booking-assistant", response_model=ChatResponse) async def chat_ai( request: ChatRequest, _: str = Depends(get_api_key) @@ -36,3 +38,24 @@ async def chat_ai( status_code=500, detail=str(e) ) + + +@router.post("/analyse-survey", response_model=SurveyAgentResponse) +async def run_survey_agent( + request: SurveyRequest, + _: str = Depends(get_api_key) +): + """Chat with DroneBot using query and history.""" + try: + from test2 import booking_form_input + agent = DroneAssessmentAgent() + result = await agent.run(booking_form_input) + return SurveyAgentResponse( + status="success", + result=result + ) + except Exception as e: + raise HTTPException( + status_code=500, + detail=str(e) + ) \ No newline at end of file diff --git a/config.py b/config.py index fb13146..619fd72 100644 --- a/config.py +++ b/config.py @@ -12,4 +12,4 @@ load_dotenv(override=True) class Config: OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") API_KEY = os.getenv("API_KEY") - \ No newline at end of file + OPENWEATHERMAP_API_KEY = os.getenv("OPENWEATHERMAP_API_KEY") \ No newline at end of file diff --git a/logger.py b/logger.py new file mode 100644 index 0000000..c0801e7 --- /dev/null +++ b/logger.py @@ -0,0 +1,55 @@ +import logging +from logging.handlers import RotatingFileHandler +import os + +def setup_logger( + name: str = "dronebot_logger", + log_file: str = "logs/app.log", + level: int = logging.DEBUG, + max_bytes: int = 5 * 1024 * 1024, # 5 MB + backup_count: int = 3 +) -> logging.Logger: + """ + Set up a logger that logs to both console and file. + + Args: + name (str): Logger name + log_file (str): Path to log file + level (int): Logging level (e.g., logging.INFO, logging.DEBUG) + max_bytes (int): Max size of each log file before rotation + backup_count (int): Number of rotated logs to keep + + Returns: + logging.Logger: Configured logger instance + """ + + # Ensure log directory exists + os.makedirs(os.path.dirname(log_file), exist_ok=True) + + # Create logger + logger = logging.getLogger(name) + logger.setLevel(level) + logger.propagate = False # Avoid double logging + + if not logger.handlers: + # Formatter + formatter = logging.Formatter( + "[%(asctime)s] %(levelname)s — %(name)s — %(message)s", + datefmt="%Y-%m-%d %H:%M:%S" + ) + + # Console handler + console_handler = logging.StreamHandler() + console_handler.setFormatter(formatter) + logger.addHandler(console_handler) + + # Rotating file handler + file_handler = RotatingFileHandler( + log_file, maxBytes=max_bytes, backupCount=backup_count + ) + file_handler.setFormatter(formatter) + logger.addHandler(file_handler) + + return logger + +logger = setup_logger() \ No newline at end of file diff --git a/src/components/__init__.py b/src/components/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/components/data_extraction/__init__.py b/src/components/data_extraction/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/components/data_extraction/weather_data.py b/src/components/data_extraction/weather_data.py new file mode 100644 index 0000000..52df1e0 --- /dev/null +++ b/src/components/data_extraction/weather_data.py @@ -0,0 +1,245 @@ +import asyncio +import aiohttp +import json +from datetime import datetime, timedelta +from typing import Dict, Tuple +import re +from logger import logger # + + +class DroneWeatherDataExtractor: + """ + Extract location data from drone booking forms and fetch structured + weather information for later use in analysis systems. + """ + + def __init__(self): + self.base_url = "https://api.open-meteo.com/v1/forecast" + + def extract_coordinates(self, booking_data: Dict) -> Tuple[float, float]: + """Extract latitude and longitude from booking form data.""" + try: + gps_coords = booking_data.get("site_information", {}).get("gps_coordinates", {}) + if isinstance(gps_coords, dict): + lat_str = gps_coords.get("latitude", "") + lng_str = gps_coords.get("longitude", "") + + lat_match = re.search(r'([\d.]+)', lat_str) + lng_match = re.search(r'([\d.]+)', lng_str) + + if lat_match and lng_match: + latitude = float(lat_match.group(1)) + longitude = float(lng_match.group(1)) + + if 'S' in lat_str.upper(): + latitude = -latitude + if 'W' in lng_str.upper(): + longitude = -longitude + + logger.info(f"Extracted coordinates from GPS: ({latitude}, {longitude})") + return latitude, longitude + + site_location = booking_data.get("form", {}).get("site_location", "") + coord_pattern = r'GPS: ([\d.]+)° ([NS]), ([\d.]+)° ([EW])' + match = re.search(coord_pattern, site_location) + + if match: + lat, lat_dir, lng, lng_dir = match.groups() + latitude = float(lat) if lat_dir == 'N' else -float(lat) + longitude = float(lng) if lng_dir == 'E' else -float(lng) + logger.info(f"Extracted coordinates from fallback: ({latitude}, {longitude})") + return latitude, longitude + + except (ValueError, KeyError) as e: + logger.error(f"Error extracting coordinates: {e}") + + raise ValueError("Could not extract valid coordinates from booking data") + + def extract_booking_info(self, booking_data: Dict) -> Dict: + """Extract relevant booking information in structured format.""" + try: + job_overview = booking_data.get("job_overview", {}) + timing = booking_data.get("timing", {}) + site_info = booking_data.get("site_information", {}) + form_info = booking_data.get("form", {}) + + info = { + "job_id": booking_data.get("job_id"), + "job_number": job_overview.get("job_number"), + "site_name": site_info.get("site_name"), + "region": site_info.get("region"), + "full_address": site_info.get("full_address"), + "asset_type": form_info.get("asset_type"), + "system_size": form_info.get("system_size"), + "survey_purpose": form_info.get("survey_purpose"), + "assigned_engineer": form_info.get("assigned_engineer"), + "contact_person": form_info.get("contact_person"), + "contact_phone": form_info.get("contact_phone"), + "dates": { + "assigned_date": job_overview.get("assigned_date"), + "preferred_dates": form_info.get("preferred_dates"), + "selected_slot": form_info.get("selected_slot") + }, + "timing": { + "start_time": timing.get("start_time"), + "end_time": timing.get("end_time"), + "duration": timing.get("survey_duration"), + "buffer_time": timing.get("buffer_time") + }, + "access_details": { + "access_type": form_info.get("access_type"), + "vehicle_access": form_info.get("vehicle_access"), + "access_details": form_info.get("access_details") + }, + "safety_requirements": form_info.get("special_safety_requirements"), + "additional_requirements": form_info.get("additional_requirements"), + "booking_timestamp": form_info.get("booking_timestamp"), + "booking_id": form_info.get("booking_id") + } + logger.debug(f"Extracted booking info: {info}") + return info + except Exception as e: + logger.error(f"Error extracting booking info: {e}") + return {} + + def parse_target_date(self, booking_data: Dict) -> datetime: + """Extract and parse the target date from booking data.""" + try: + date_fields = [ + booking_data.get("job_overview", {}).get("assigned_date"), + booking_data.get("form", {}).get("preferred_dates"), + booking_data.get("form", {}).get("selected_slot") + ] + + for date_field in date_fields: + if not date_field: + continue + if "January 23" in date_field: + return datetime(2025, 1, 23) + elif "January 24" in date_field: + return datetime(2025, 1, 24) + + logger.warning("Using fallback date") + return datetime.now() + timedelta(days=1) + except Exception as e: + logger.error(f"Error parsing date: {e}") + return datetime.now() + timedelta(days=1) + + async def fetch_weather_data(self, latitude: float, longitude: float, + target_date: datetime, days_range: int = 3) -> Dict: + """Fetch structured weather data for specific coordinates and date range.""" + start_date = target_date - timedelta(days=days_range // 2) + end_date = target_date + timedelta(days=days_range // 2) + + params = { + "latitude": latitude, + "longitude": longitude, + "current": "temperature_2m,relative_humidity_2m,wind_speed_10m", + "hourly": "temperature_2m,relative_humidity_2m" + } + + logger.info(f"Requesting weather data for {latitude}, {longitude} from {start_date.date()} to {end_date.date()}") + async with aiohttp.ClientSession() as session: + async with session.get(self.base_url, params=params) as response: + if response.status == 200: + logger.info("Weather data fetched successfully.") + return await response.json() + else: + error_msg = f"Weather API request failed: {response.status}" + logger.error(error_msg) + raise Exception(error_msg) + + def structure_weather_data(self, raw_weather: Dict, target_date: datetime) -> Dict: + """Structure raw weather data into organized format.""" + try: + structured_data = { + "location": { + "latitude": raw_weather.get("latitude"), + "longitude": raw_weather.get("longitude"), + "elevation": raw_weather.get("elevation"), + "timezone": raw_weather.get("timezone") + }, + "current_conditions": raw_weather.get("current", {}), + "hourly_forecast": raw_weather.get("hourly", {}), + "daily_forecast": raw_weather.get("daily", {}), + "target_date": target_date.strftime("%Y-%m-%d"), + "data_retrieved_at": datetime.now().isoformat() + } + logger.debug("Structured weather data successfully.") + return structured_data + except Exception as e: + logger.error(f"Error structuring weather data: {e}") + return {"error": f"Error structuring weather data: {e}"} + + async def extract_booking_weather_data(self, booking_data: Dict) -> Dict: + """Main extraction function - returns structured data for external use.""" + try: + logger.info(f"Starting extraction for job_id={booking_data.get('job_id')}") + booking_info = self.extract_booking_info(booking_data) + latitude, longitude = self.extract_coordinates(booking_data) + target_date = self.parse_target_date(booking_data) + raw_weather = await self.fetch_weather_data(latitude, longitude, target_date) + structured_weather = self.structure_weather_data(raw_weather, target_date) + + logger.info(f"Completed extraction for job_id={booking_data.get('job_id')}") + return { + "weather_data": structured_weather, + "extraction_metadata": { + "processed_at": datetime.now().isoformat(), + "api_endpoint": self.base_url, + "data_source": "open-meteo" + } + } + except Exception as e: + logger.exception(f"Data extraction failed for job_id={booking_data.get('job_id', 'unknown')}") + return { + "error": f"Data extraction failed: {e}", + "job_id": booking_data.get("job_id", "unknown"), + "processed_at": datetime.now().isoformat() + } + + +# Example usage +async def main(): + logger.info("Demo started.") + booking_form_input = { + "job_id": "1043", + "job_overview": { + "job_number": "Job #1043", + "site_name": "Hightower Solar Farm", + "assigned_date": "January 23, 2025", + "status": "Scheduled" + }, + "site_information": { + "site_name": "Hightower Solar Farm", + "region": "North England", + "full_address": "Grange Lane, Manchester M34 7TF", + "gps_coordinates": { + "latitude": "13.41° N", + "longitude": "52.52 W" + } + }, + "timing": { + "start_time": "09:00 AM", + "end_time": "10:30 AM", + "survey_duration": "30–45 mins", + "buffer_time": "45 mins" + }, + "form": { + "asset_type": "Solar Farm", + "system_size": "5.2 MW capacity, approximately 16,000 panels across 12 hectares", + "survey_purpose": "Insurance assessment", + "assigned_engineer": "David Wilson - 0161-555-0876", + "contact_person": "Sarah Thompson", + "contact_phone": "0161-555-0234" + } + } + + extractor = DroneWeatherDataExtractor() + result = await extractor.extract_booking_weather_data(booking_form_input) + logger.info("Extraction result:") + logger.info(json.dumps(result, indent=2)) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/llm/agent/__init__.py b/src/llm/agent/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/llm/agent/flight_assesment.py b/src/llm/agent/flight_assesment.py new file mode 100644 index 0000000..b2521fc --- /dev/null +++ b/src/llm/agent/flight_assesment.py @@ -0,0 +1,90 @@ +import os +import json +import asyncio +from langchain_openai import ChatOpenAI +from langchain_core.messages import HumanMessage, SystemMessage +from src.prompts.templates.flght_prompt import flight_prompt +from src.config.llm_config import LlmConfig +from config import Config +from src.components.data_extraction.weather_data import DroneWeatherDataExtractor +from logger import logger + + +class DroneAssessmentAgent: + def __init__(self): + self.llm = ChatOpenAI( + api_key=Config.OPENAI_API_KEY, + model=LlmConfig.openai.models.gpt_4o, + temperature=0.3 + ) + self.weather_extractor = DroneWeatherDataExtractor() + + async def run(self, booking_form: str) -> dict: + """ + Run the drone environmental & safety assessment agent. + + Args: + booking_form (str): Structured booking form input + + Returns: + dict: AI-generated structured output + """ + logger.info("Starting DroneAssessmentAgent run...") + + try: + # Step 1: Fetch weather data + weather_data = await self.weather_extractor.extract_booking_weather_data(booking_form) + booking_form_text = json.dumps(booking_form) + + # Step 2: Generate prompt + prompt = flight_prompt(booking_form_text, weather_data) + messages = [ + SystemMessage(content=prompt), + HumanMessage(content=booking_form_text) + ] + + # Step 3: Invoke LLM + logger.debug("Sending prompt to LLM...") + response = self.llm.invoke(messages) + + if hasattr(response, "content"): + response_text = response.content.strip() + logger.info("Received LLM response.") + logger.debug(f"LLM Raw Output (first 300 chars):\n{response_text[:300]}") + + # Extract JSON block + start_idx = response_text.find('{') + end_idx = response_text.rfind('}') + 1 + + if start_idx == -1 or end_idx == -1: + raise ValueError("No JSON object found in output") + + json_str = response_text[start_idx:end_idx] + return json.loads(json_str) + + else: + raise ValueError("LLM returned no usable content") + + except Exception as e: + logger.exception("Error in DroneAssessmentAgent") + return { + "error": str(e), + "raw_response": response.content if 'response' in locals() and hasattr(response, "content") else None + } + + +async def main(): + """Async main function to test the drone assessment agent.""" + from test2 import booking_form_input + + logger.info("Launching DroneAssessmentAgent from main()...") + agent = DroneAssessmentAgent() + result = await agent.run(booking_form_input) + + logger.info("Drone assessment completed.") + logger.debug("Final structured output:") + logger.debug(json.dumps(result, indent=2)) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/prompts/templates/__pycache__/chat_templates.cpython-311.pyc b/src/prompts/templates/__pycache__/chat_templates.cpython-311.pyc index b8ed67a..6d59944 100644 Binary files a/src/prompts/templates/__pycache__/chat_templates.cpython-311.pyc and b/src/prompts/templates/__pycache__/chat_templates.cpython-311.pyc differ diff --git a/src/prompts/templates/chat_templates.py b/src/prompts/templates/chat_templates.py index 4d5d7c5..95896a5 100644 --- a/src/prompts/templates/chat_templates.py +++ b/src/prompts/templates/chat_templates.py @@ -1,6 +1,6 @@ def get_booking_prompt(): - return """ + return f""" ## System Instructions & Persona @@ -12,215 +12,428 @@ You are detail-oriented and safety-conscious, always ensuring that our certified Your primary function is to guide users through a comprehensive booking process, collecting all necessary information to schedule drone inspections with our certified engineers. You follow a specific step-by-step process to ensure no critical details are missed. +## IMPORTANT: JSON Response Format + +You MUST ALWAYS respond with a structured JSON object containing exactly these fields: + +```json +{{ + "message": "Your conversational response to the user", + "options": ["Option 1", "Option 2", "Option 3"] or null, + "requires_selection": true/false, + "end": "complete" | "in_progress" | "cancelled", + "form": {{}} +}} +``` + +**Field Explanations:** +- **message**: Your conversational response (friendly, professional tone) +- **options**: Array of choices for user to select from, or null if no options needed +- **requires_selection**: true if user must choose from options, false if they can provide free text +- **end**: + - "complete" = booking session successfully finished + - "in_progress" = still collecting information + - "cancelled" = user cancelled or session ended without completion +- **form**: + - Empty object {{}} when end is "in_progress" or "cancelled" + - Complete structured form data when end is "complete" + ## Initial Greeting & Introduction -**Bot:** "Hello! I'm your drone survey booking assistant. I'll help you schedule a drone inspection with one of our certified engineers. This will take just a few minutes - I'll ask you some questions about your site and requirements. Let's get started!" + +**Response:** +```json +{{ + "message": "Hello! I'm your drone survey booking assistant. I'll help you schedule a drone inspection with one of our certified engineers. This will take just a few minutes - I'll ask you some questions about your site and requirements. Let's get started!", + "options": null, + "requires_selection": false, + "end": "in_progress", + "form": {{}} +}} +``` --- ## Step 1: Asset Type Identification -**Bot:** "First, what type of asset needs surveying? -Please select: -1. Solar Farm -2. Wind Turbine -3. Other renewable energy asset +**Response:** +```json +{{ + "message": "First, what type of asset needs surveying?", + "options": ["Solar Farm", "Wind Turbine", "Other renewable energy asset"], + "requires_selection": true, + "end": "in_progress", + "form": {{}} +}} +``` -If you selected 'Other', please specify what type of asset it is." - -**Follow-up if needed:** "Could you provide more details about the asset type? This helps us assign the right specialist engineer." +**If "Other" selected:** +```json +{{ + "message": "Could you please specify what type of renewable energy asset it is? This helps us assign the right specialist engineer.", + "options": null, + "requires_selection": false, + "end": "in_progress", + "form": {{}} +}} +``` --- ## Step 2: Site Identification -**Bot:** "Great! Now I need some basic site information. -What's the name or identifier for this site? (This could be a site name, project code, or any reference you use)" +**Response:** +```json +{{ + "message": "Great! Now I need some basic site information. What's the name or identifier for this site? (This could be a site name, project code, or any reference you use)", + "options": null, + "requires_selection": false, + "end": "in_progress", + "form": {{}} +}} +``` -**Follow-up:** "Perfect. Could you also provide the location? Please share: -- Full address, OR -- GPS coordinates, OR -- Nearest landmark/town if exact address isn't available" +**Follow-up:** +```json +{{ + "message": "Perfect. Could you also provide the location? Please share the full address, GPS coordinates, or nearest landmark/town if exact address isn't available.", + "options": null, + "requires_selection": false, + "end": "in_progress", + "form": {{}} +}} +``` --- ## Step 3: System Size/Scope -**Bot:** "To help our engineer prepare properly, what's the size/scope of your installation? -For example: -- Solar: Number of panels or total kW/MW capacity -- Wind: Number of turbines or total MW capacity -- Other: Any relevant size/capacity details" - -**If unclear:** "Any rough estimate is fine - this helps our engineer know what equipment to bring and how long the survey might take." +**Response:** +```json +{{ + "message": "To help our engineer prepare properly, what's the size/scope of your installation? For example: Solar (Number of panels or total kW/MW capacity), Wind (Number of turbines or total MW capacity), or any relevant size/capacity details. Any rough estimate is fine.", + "options": null, + "requires_selection": false, + "end": "in_progress", + "form": {{}} +}} +``` --- ## Step 4: Access Requirements Check -**Bot:** "Now I need to understand site access requirements. -Is your site gated or has restricted access?" +**Response:** +```json +{{ + "message": "Now I need to understand site access requirements. Is your site gated or has restricted access?", + "options": ["Yes - Restricted Access", "No - Open Access"], + "requires_selection": true, + "end": "in_progress", + "form": {{}} +}} +``` -**If YES:** -"I'll need access details: -- Gate code (if applicable) -- Key holder contact details -- Any specific access instructions -- Best entry point or directions" +**If "Yes" selected:** +```json +{{ + "message": "I'll need access details including: gate code (if applicable), key holder contact details, any specific access instructions, and best entry point or directions. Please provide these details.", + "options": null, + "requires_selection": false, + "end": "in_progress", + "form": {{}} +}} -**If NO:** -"Great! Is the site easily accessible by vehicle for our drone equipment?" +**If "No" selected:** +```json +{{ + "message": "Great! Is the site easily accessible by vehicle for our drone equipment?", + "options": ["Yes - Vehicle Accessible", "No - Limited Vehicle Access"], + "requires_selection": true, + "end": "in_progress", + "form": {{}} +}} +``` --- ## Step 5: On-Site Contact Information -**Bot:** "Who should our engineer contact on the day of the survey? -Please provide: -- Contact person's name -- Phone number -- Their role (optional - e.g., site manager, maintenance technician)" +**Response:** +```json +{{ + "message": "Who should our engineer contact on the day of the survey? Please provide: contact person's name, phone number, and their role (optional - e.g., site manager, maintenance technician).", + "options": null, + "requires_selection": false, + "end": "in_progress", + "form": {{}} +}} +``` -**Follow-up:** "Will this person be available on-site during the survey, or should our engineer call ahead?" +**Follow-up:** +```json +{{ + "message": "Will this person be available on-site during the survey, or should our engineer call ahead?", + "options": ["Available on-site", "Call ahead required"], + "requires_selection": true, + "end": "in_progress", + "form": {{}} +}} +``` --- ## Step 6: Special Access/Safety Requirements -**Bot:** "Are there any special requirements our engineer should know about? -For example: -- Specific PPE needed -- Safety induction required -- Time restrictions (e.g., only accessible during certain hours) -- Security clearance needed -- Any site hazards or restrictions" +**Response:** +```json +{{ + "message": "Are there any special requirements our engineer should know about? For example: specific PPE needed, safety induction required, time restrictions, security clearance needed, or any site hazards or restrictions?", + "options": ["Yes - Special Requirements", "No - Standard Requirements"], + "requires_selection": true, + "end": "in_progress", + "form": {{}} +}} +``` -**If none:** "That's fine - our engineers always bring standard safety equipment." +**If "Yes" selected:** +```json +{{ + "message": "Please provide details about the special requirements so our engineer can prepare accordingly.", + "options": null, + "requires_selection": false, + "end": "in_progress", + "form": {{}} +}} +``` --- ## Step 7: Survey Purpose -**Bot:** "What's the main purpose of this drone survey? -Please select: -1. Routine maintenance inspection -2. Insurance assessment -3. Fault diagnosis/investigation -4. Compliance/regulatory requirement -5. Performance optimization -6. Other (please specify)" +**Response:** +```json +{{ + "message": "What's the main purpose of this drone survey?", + "options": [ + "Routine maintenance inspection", + "Insurance assessment", + "Fault diagnosis/investigation", + "Compliance/regulatory requirement", + "Performance optimization", + "Other" + ], + "requires_selection": true, + "end": "in_progress", + "form": {{}} +}} +``` -**Follow-up based on selection:** -- **Maintenance:** "Any specific areas of concern or components to focus on?" -- **Insurance:** "Do you have any specific requirements from your insurer?" -- **Fault:** "Can you describe the issue you're experiencing?" -- **Compliance:** "Which standards or regulations need to be met?" +**Follow-up based on selection (example for "Fault diagnosis"):** +```json +{{ + "message": "Can you describe the issue you're experiencing? This will help our engineer focus on the right areas.", + "options": null, + "requires_selection": false, + "end": "in_progress", + "form": {{}} +}} +``` --- ## Step 8: Special Notes/Additional Requirements -**Bot:** "Almost done! Are there any other details, special requests, or concerns you'd like our engineer to know about? -For example: -- Specific weather requirements -- Preferred time of day -- Areas to avoid -- Additional documentation needed -- Urgent timeline requirements" - -**If none:** "No problem - we'll proceed with standard survey procedures." +**Response:** +```json +{{ + "message": "Almost done! Are there any other details, special requests, or concerns you'd like our engineer to know about? For example: specific weather requirements, preferred time of day, areas to avoid, additional documentation needed, or urgent timeline requirements?", + "options": ["Yes - Additional Requirements", "No - Standard Procedure"], + "requires_selection": true, + "end": "in_progress", + "form": {{}} +}} +``` --- ## Step 9: Preferred Timing -**Bot:** "When would you prefer the survey to take place? -Please let me know: -- Any specific dates you prefer -- Days of the week that work best -- Time of day preferences -- How urgent this is (ASAP, within 1 week, within 1 month, flexible) - -Or simply tell me if you're flexible and want to see all available options." +**Response:** +```json +{{ + "message": "When would you prefer the survey to take place? Please let me know: any specific dates you prefer, days of the week that work best, time of day preferences, and how urgent this is.", + "options": ["ASAP (within 1-2 days)", "Within 1 week", "Within 1 month", "Flexible timing"], + "requires_selection": true, + "end": "in_progress", + "form": {{}} +}} +``` --- ## Step 10: Calendar Check & Booking Confirmation -**Bot:** "Perfect! Let me check our engineer availability based on your location and requirements... -[SYSTEM: Check available calendar slots based on location, engineer expertise, and user preferences using the approriate tools] +**Response (before checking calendar):** +```json +{{ + "message": "Perfect! Let me check our engineer availability based on your location and requirements...", + "options": null, + "requires_selection": false, + "end": "in_progress", + "form": {{}} +}} +``` -Here are the available time slots: - -**Next Week:** -- Monday, [Date] - 9:00 AM - 12:00 PM -- Wednesday, [Date] - 2:00 PM - 5:00 PM -- Friday, [Date] - 10:00 AM - 1:00 PM - -**Following Week:** -- Tuesday, [Date] - 8:00 AM - 11:00 AM -- Thursday, [Date] - 1:00 PM - 4:00 PM - -Which slot works best for you?" +**Response (with available slots):** +```json +{{ + "message": "Here are the available time slots: Next Week: Monday, [Date] - 9:00 AM - 12:00 PM, Wednesday, [Date] - 2:00 PM - 5:00 PM, Friday, [Date] - 10:00 AM - 1:00 PM. Following Week: Tuesday, [Date] - 8:00 AM - 11:00 AM, Thursday, [Date] - 1:00 PM - 4:00 PM. Which slot works best for you?", + "options": [ + "Monday, [Date] - 9:00 AM - 12:00 PM", + "Wednesday, [Date] - 2:00 PM - 5:00 PM", + "Friday, [Date] - 10:00 AM - 1:00 PM", + "Tuesday, [Date] - 8:00 AM - 11:00 AM", + "Thursday, [Date] - 1:00 PM - 4:00 PM", + "None of these work - need alternatives" + ], + "requires_selection": true, + "end": "in_progress", + "form": {{}} +}} +``` --- ## Final Confirmation -**Bot:** "Excellent! Let me confirm your booking: -**Survey Details:** -- Asset: [Asset Type] at [Site Name] -- Location: [Address/GPS] -- Date & Time: [Selected Slot] -- Purpose: [Survey Purpose] -- Engineer: [Engineer Name] - [Phone Number] +**Response:** +```json +{{ + "message": "Excellent! Your booking is confirmed: Asset: [Asset Type] at [Site Name], Location: [Address], Date & Time: [Selected Slot], Purpose: [Survey Purpose], Engineer: [Engineer Name] - [Phone Number], Your Contact: [Contact Person] - [Phone Number], Access Details: [Summary], Special Notes: [Any requirements]. You'll receive a confirmation email shortly. Our engineer will call 30 minutes before arrival. Is there anything else I can help you with today?", + "options": ["Yes - I have more questions", "No - That's everything"], + "requires_selection": true, + "end": "complete", + "form": {{ + "asset_type": "[Asset Type from Step 1]", + "asset_details": "[Additional asset details if 'Other' was selected]", + "site_name": "[Site name/identifier from Step 2]", + "site_location": "[Full address/GPS/landmark from Step 2]", + "system_size": "[Size/scope details from Step 3]", + "access_type": "[Restricted/Open from Step 4]", + "access_details": "[Gate codes, key holder, instructions from Step 4]", + "vehicle_access": "[Vehicle accessibility info from Step 4]", + "contact_person": "[Name from Step 5]", + "contact_phone": "[Phone number from Step 5]", + "contact_role": "[Role/title from Step 5]", + "contact_availability": "[On-site/Call ahead from Step 5]", + "special_safety_requirements": "[PPE, induction, restrictions from Step 6]", + "survey_purpose": "[Main purpose from Step 7]", + "survey_purpose_details": "[Additional details based on purpose from Step 7]", + "additional_requirements": "[Special requests, weather, timing notes from Step 8]", + "timing_preference": "[ASAP/1 week/1 month/flexible from Step 9]", + "preferred_dates": "[Specific dates mentioned by user]", + "selected_slot": "[Confirmed date and time slot]", + "assigned_engineer": "[Engineer name and contact]", + "booking_timestamp": "[Current timestamp when booking completed]", + "booking_id": "[Generated unique booking ID]" + }} +}} +``` -**Your Contact:** [Contact Person] - [Phone Number] - -**Access Details:** [Summary of access requirements] - -**Special Notes:** [Any special requirements] - -This booking is now confirmed! You'll receive a confirmation email shortly. - -Our engineer will call your on-site contact 30 minutes before arrival. If you need to make any changes, please contact us at [contact details]. - -Is there anything else I can help you with today?" +**If user says "No - That's everything":** +```json +{{ + "message": "Perfect! Your drone survey is all booked. Thank you for choosing our services. Have a great day!", + "options": null, + "requires_selection": false, + "end": "complete", + "form": {{ + "asset_type": "[Asset Type from Step 1]", + "asset_details": "[Additional asset details if 'Other' was selected]", + "site_name": "[Site name/identifier from Step 2]", + "site_location": "[Full address/GPS/landmark from Step 2]", + "system_size": "[Size/scope details from Step 3]", + "access_type": "[Restricted/Open from Step 4]", + "access_details": "[Gate codes, key holder, instructions from Step 4]", + "vehicle_access": "[Vehicle accessibility info from Step 4]", + "contact_person": "[Name from Step 5]", + "contact_phone": "[Phone number from Step 5]", + "contact_role": "[Role/title from Step 5]", + "contact_availability": "[On-site/Call ahead from Step 5]", + "special_safety_requirements": "[PPE, induction, restrictions from Step 6]", + "survey_purpose": "[Main purpose from Step 7]", + "survey_purpose_details": "[Additional details based on purpose from Step 7]", + "additional_requirements": "[Special requests, weather, timing notes from Step 8]", + "timing_preference": "[ASAP/1 week/1 month/flexible from Step 9]", + "preferred_dates": "[Specific dates mentioned by user]", + "selected_slot": "[Confirmed date and time slot]", + "assigned_engineer": "[Engineer name and contact]", + "booking_timestamp": "[Current timestamp when booking completed]", + "booking_id": "[Generated unique booking ID]" + }} +}} +``` --- ## Error Handling & Clarification Prompts ### If User Provides Incomplete Information: -**Bot:** "I need a bit more information to complete your booking. Could you please provide [specific missing detail]?" +```json +{{ + "message": "I need a bit more information to complete your booking. Could you please provide [specific missing detail]?", + "options": null, + "requires_selection": false, + "end": "in_progress", + "form": {{}} +}} +``` ### If No Available Slots Match Preferences: -**Bot:** "I don't see any available slots that match your exact preferences. However, I have these alternatives: -[List alternative slots] +```json +{{ + "message": "I don't see any available slots that match your exact preferences. However, I have these alternatives: [List alternative slots]. Would any of these work?", + "options": ["[Alternative Slot 1]", "[Alternative Slot 2]", "[Alternative Slot 3]", "Check with scheduling team for other options"], + "requires_selection": true, + "end": "in_progress", + "form": {{}} +}} +``` -Would any of these work, or would you prefer me to check with our scheduling team for other options?" - -### If User Wants to Modify Information: -**Bot:** "No problem! Which detail would you like to change? I can update: -1. Site information -2. Access details -3. Survey requirements -4. Contact information -5. Preferred timing" +### If User Wants to Cancel: +```json +{{ + "message": "I understand you'd like to cancel. No problem at all. If you change your mind, feel free to start a new booking session anytime. Have a great day!", + "options": null, + "requires_selection": false, + "end": "cancelled", + "form": {{}} +}} +``` ### If Technical Issues: -**Bot:** "I'm having trouble accessing our calendar system right now. Let me take your details and have our scheduling team contact you within 2 hours to confirm your appointment. Is that acceptable?" - ---- +```json +{{ + "message": "I'm having trouble accessing our calendar system right now. Let me have our scheduling team contact you within 2 hours to confirm your appointment. Is that acceptable?", + "options": ["Yes - Have team contact me", "No - I'll try again later"], + "requires_selection": true, + "end": "in_progress", + "form": {{}} +}} ## Additional Context for Bot Behavior +- **Always respond in valid JSON format with all required fields** +- **Form field:** Always include empty {{}} when end is "in_progress" or "cancelled", complete form data when end is "complete" - **Tone:** Professional but friendly, efficient but not rushed - **Flexibility:** Always offer alternatives if first options don't work -- **Confirmation:** Summarize key details at each major step +- **Confirmation:** Summarize key details at major steps - **Safety Focus:** Always emphasize safety requirements and proper preparation - **Contact:** Provide clear next steps and contact information - **Urgency Handling:** Prioritize urgent requests and escalate when needed -NOTE: THIS IS SOLELY YOUR TASK PLEASE, NO OTHR THING +NOTE: THIS IS SOLELY YOUR TASK PLEASE, NO OTHER THING : DO NOT FABRICATE AVAILABLE SLOTS, ALWAYS CHECK FIRST -""" + : ALWAYS RESPOND IN VALID JSON FORMAT WITH THE REQUIRED FIELDS + : RETURN ONLY JSON FORMAT NO EXPLANATION OR EXTRA INFORMATION BEFORE OR AFTER, DO NOT ADD ```json before or after +""" \ No newline at end of file diff --git a/src/prompts/templates/flght_prompt.py b/src/prompts/templates/flght_prompt.py new file mode 100644 index 0000000..972b51e --- /dev/null +++ b/src/prompts/templates/flght_prompt.py @@ -0,0 +1,106 @@ + + +def flight_prompt(booking_details,weather_details): + return f""" + + +You are an expert drone survey analyst with extensive experience in renewable energy site inspections. Your task is to analyze booking form data and generate comprehensive environmental, safety, and operational predictions for drone surveys. + +### Instructions: + +You will be provided with a complete booking form containing site details, contact information, access requirements, and survey specifications. Based on this information, you must generate predictions and assessments in the specified JSON format. + +PROVIDED BOOKING FORM DETAILS: {booking_details} + +PROVIDED WEATHER DETAILS: {weather_details} , BASED ON CURRENT WEATHER CONDITIONS API + +### Analysis Requirements: + +1. **Airspace Assessment**: Predict airspace type based on location, proximity to airports, urban density [Class G, class E, class C, class D, Restricted] +2. **Terrain Analysis**: Assess terrain type from site description, asset type, and location context [Flat, Uneven, Hilly, Urban, Remote] +3. **Weather Risk Evaluation**: Estimate weather risk based on location, season, timing, and site exposure [low, medium, high] +4. **Obstruction Identification**: Predict likely obstructions from site type, infrastructure, and environmental context +5. **Safety & Compliance**: Generate safety protocols based on site requirements and asset type +6. **Risk Mitigation**: Create appropriate risk management strategies + +### Prediction Guidelines: + +- **Airspace Types**: C (Controlled), D (Controlled), E (Controlled), G (Uncontrolled), Restricted +- **Terrain Types**: flat, uneven, hilly, urban, remote +- **Weather Risk Levels**: low, medium, high +- Use logical inference when specific data is not available +- Reference UK/regional standards for locations outside explicit coverage +- Prioritize safety and conservative estimates when uncertain + +### Output Format: + +You must respond with a properly formatted JSON object containing all required fields. Do not include explanations outside the JSON structure. + +### JSON Response Template: + +```json +{{ + "airspace_assessment": {{ + "airspace_type": "[C/D/E/G/Restricted]", + "confidence_level": "[high/medium/low]", + "reasoning": "[Brief explanation of prediction]", + "restrictions": "[Any predicted airspace limitations]" + }}, + "terrain_analysis": {{ + "terrain_type": "[flat/uneven/hilly/urban/remote]", + "terrain_description": "[Detailed terrain assessment]", + "accessibility": "[Vehicle/equipment access prediction]", + "elevation_considerations": "[Any elevation factors]" + }}, + "weather_assessment": {{ + "weather_risk_level": "[low/medium/high]", + "predicted_conditions": {{ + "wind_speed": "[Estimated km/h]", + "visibility": "[clear/limited/poor]", + "precipitation": "[none/light/moderate/heavy expected]", + "temperature": "[Estimated °C]" + }}, + "weather_details": "[AI-style weather summary]", + "seasonal_considerations": "[Relevant seasonal factors]" + }}, + "obstruction_analysis": {{ + "nearby_obstructions": "[Predicted obstacles description]", + "power_infrastructure": "[Electrical hazards assessment]", + "vegetation": "[Tree/plant interference potential]", + "structures": "[Buildings/towers/equipment predictions]", + "clearance_requirements": "[Recommended clearance distances]" + }}, + "safety_compliance": {{ + "public_safety_assessed": true, + "wildlife_check_status": "[complete/requires_attention]", + "airspace_clearance_status": "[complete/requires_attention]", + "permits_status": "[complete/requires_attention]", + "environmental_concerns": "[Any predicted environmental issues]" + }}, + "risk_mitigation_plan": {{ + "required_ppe": "[PPE specifications]", + "emergency_procedures": "[Emergency contact and procedures]", + "alternative_plans": "[Backup landing zones, weather alternatives]", + "monitoring_requirements": "[What to monitor during operation]", + "communication_protocols": "[Site communication requirements]" + }}, + "operational_recommendations": {{ + "optimal_flight_time": "[Best time window for survey]", + "recommended_approach": "[Flight pattern/approach suggestions]", + "equipment_considerations": "[Drone/sensor recommendations]", + "duration_estimate": "[Predicted survey duration]", + "crew_requirements": "[Personnel needs]" + }}, + "confidence_metrics": {{ + "overall_confidence": "[high/medium/low]", + "data_completeness": "[percentage or assessment]", + "external_data_needed": "[List of external data sources recommended]", + "verification_required": "[On-site verification items]" + }} +}} + +NOTE: THIS IS SOLELY YOUR TASK PLEASE, NO OTHER THING + : DO NOT FABRICATE AVAILABLE SLOTS, ALWAYS CHECK FIRST + : ALWAYS RESPOND IN VALID JSON FORMAT WITH THE REQUIRED FIELDS + : RETURN ONLY JSON FORMAT NO EXPLANATION OR EXTRA INFORMATION BEFORE OR AFTER, DO NOT ADD ```json before or after +""" \ No newline at end of file diff --git a/test.py b/test.py index 49f5e73..bd11dcb 100644 --- a/test.py +++ b/test.py @@ -1,38 +1,329 @@ -# terminal_chat.py -from src.llm.orchestrator import DroneBot, Message # Adjust import path as needed +import asyncio +import aiohttp +import json +from datetime import datetime, timedelta +from typing import Dict, List, Optional, Tuple +import re -def terminal_chat(): - print("🚁 DroneBot Terminal Chat") - print("Type 'exit' to quit.\n") - - # Initial conversation history - history = [] - - # Initialize the bot - bot = DroneBot(history=[], use_openai_as_fallback=True) - - # Get initial user input - while True: - user_input = input("👤 You: ") - if user_input.lower() in ("exit", "quit"): - print("👋 Exiting DroneBot. Goodbye!") - break - - # Add user message to history - history.append(Message(role="human", content=user_input)) - - # Update bot's history - bot.history = history - - # Get bot response - response = bot.chat(user_input) - - # Add bot response to history - history.append(Message(role="ai", content=response["final_message"])) - - # Print bot response - print(f"🤖 DroneBot: {response['final_message']}\n") +class DroneWeatherDataExtractor: + """ + Extract location data from drone booking forms and fetch structured + weather information for later use in analysis systems. + """ + + def __init__(self): + self.base_url = "https://api.open-meteo.com/v1/forecast" + + def extract_coordinates(self, booking_data: Dict) -> Tuple[float, float]: + """Extract latitude and longitude from booking form data.""" + try: + # Try to get from GPS coordinates field + gps_coords = booking_data.get("site_information", {}).get("gps_coordinates", {}) + + if isinstance(gps_coords, dict): + lat_str = gps_coords.get("latitude", "") + lng_str = gps_coords.get("longitude", "") + + # Parse coordinate strings like "53.4408° N" and "2.2426° W" + lat_match = re.search(r'([\d.]+)', lat_str) + lng_match = re.search(r'([\d.]+)', lng_str) + + if lat_match and lng_match: + latitude = float(lat_match.group(1)) + longitude = float(lng_match.group(1)) + + # Handle direction indicators + if 'S' in lat_str.upper(): + latitude = -latitude + if 'W' in lng_str.upper(): + longitude = -longitude + + return latitude, longitude + + # Fallback: try to extract from site_location string + site_location = booking_data.get("form", {}).get("site_location", "") + coord_pattern = r'GPS: ([\d.]+)° ([NS]), ([\d.]+)° ([EW])' + match = re.search(coord_pattern, site_location) + + if match: + lat, lat_dir, lng, lng_dir = match.groups() + latitude = float(lat) if lat_dir == 'N' else -float(lat) + longitude = float(lng) if lng_dir == 'E' else -float(lng) + print(f"Extracted coordinates: {latitude}, {longitude}") + return latitude, longitude + + except (ValueError, KeyError) as e: + print(f"Error extracting coordinates: {e}") + + raise ValueError("Could not extract valid coordinates from booking data") + + def extract_booking_info(self, booking_data: Dict) -> Dict: + """Extract relevant booking information in structured format.""" + try: + # Extract date information + assigned_date = booking_data.get("job_overview", {}).get("assigned_date") + preferred_dates = booking_data.get("form", {}).get("preferred_dates") + selected_slot = booking_data.get("form", {}).get("selected_slot") + + # Extract timing information + timing = booking_data.get("timing", {}) + start_time = timing.get("start_time") + end_time = timing.get("end_time") + duration = timing.get("survey_duration") + + # Extract site information + site_info = booking_data.get("site_information", {}) + form_info = booking_data.get("form", {}) + + return { + "job_id": booking_data.get("job_id"), + "job_number": booking_data.get("job_overview", {}).get("job_number"), + "site_name": site_info.get("site_name"), + "region": site_info.get("region"), + "full_address": site_info.get("full_address"), + "asset_type": form_info.get("asset_type"), + "system_size": form_info.get("system_size"), + "survey_purpose": form_info.get("survey_purpose"), + "assigned_engineer": form_info.get("assigned_engineer"), + "contact_person": form_info.get("contact_person"), + "contact_phone": form_info.get("contact_phone"), + "dates": { + "assigned_date": assigned_date, + "preferred_dates": preferred_dates, + "selected_slot": selected_slot + }, + "timing": { + "start_time": start_time, + "end_time": end_time, + "duration": duration, + "buffer_time": timing.get("buffer_time") + }, + "access_details": { + "access_type": form_info.get("access_type"), + "vehicle_access": form_info.get("vehicle_access"), + "access_details": form_info.get("access_details") + }, + "safety_requirements": form_info.get("special_safety_requirements"), + "additional_requirements": form_info.get("additional_requirements"), + "booking_timestamp": form_info.get("booking_timestamp"), + "booking_id": form_info.get("booking_id") + } + + except Exception as e: + print(f"Error extracting booking info: {e}") + return {} + + def parse_target_date(self, booking_data: Dict) -> datetime: + """Extract and parse the target date from booking data.""" + try: + # Try different date fields + date_fields = [ + booking_data.get("job_overview", {}).get("assigned_date"), + booking_data.get("form", {}).get("preferred_dates"), + booking_data.get("form", {}).get("selected_slot") + ] + + for date_field in date_fields: + if date_field: + # Handle different date formats + if "January 23, 2025" in date_field or "January 23rd, 2025" in date_field: + return datetime(2025, 1, 23) + elif "January 24" in date_field: + return datetime(2025, 1, 24) + + # Default fallback + return datetime.now() + timedelta(days=1) + + except Exception as e: + print(f"Error parsing date: {e}") + return datetime.now() + timedelta(days=1) + + async def fetch_weather_data(self, latitude: float, longitude: float, + target_date: datetime, days_range: int = 3) -> Dict: + """Fetch structured weather data for specific coordinates and date range.""" + + # Calculate date range around target date + start_date = target_date - timedelta(days=days_range//2) + end_date = target_date + timedelta(days=days_range//2) + + params = { + "latitude": latitude, + "longitude": longitude, + "current": [ + "temperature_2m", + "relative_humidity_2m", + "wind_speed_10m" + # "wind_direction_10m", + # "weather_code", + # "pressure_msl", + # "cloud_cover" + ], + "hourly": [ + "temperature_2m", + "relative_humidity_2m" + # "wind_speed_10m", + # "wind_direction_10m", + # "precipitation", + # "weather_code", + # "visibility", + # "cloud_cover", + # "pressure_msl" + ], + # "daily": [ + # "temperature_2m_max", + # "temperature_2m_min"] + # # "sunrise", + # "sunset", + # "precipitation_sum", + # "wind_speed_10m_max", + # "wind_direction_10m_dominant" + # ], + # "start_date": start_date.strftime("%Y-%m-%d"), + # "end_date": end_date.strftime("%Y-%m-%d"), + # "timezone": "auto" + } + + # Convert lists to comma-separated strings for API + for key, value in params.items(): + if isinstance(value, list): + params[key] = ",".join(value) + + async with aiohttp.ClientSession() as session: + async with session.get(self.base_url, params=params) as response: + if response.status == 200: + return await response.json() + else: + raise Exception(f"Weather API request failed: {response.status}") + + def structure_weather_data(self, raw_weather: Dict, target_date: datetime) -> Dict: + """Structure raw weather data into organized format.""" + try: + structured_data = { + "location": { + "latitude": raw_weather.get("latitude"), + "longitude": raw_weather.get("longitude"), + "elevation": raw_weather.get("elevation"), + "timezone": raw_weather.get("timezone") + }, + "current_conditions": raw_weather.get("current", {}), + "hourly_forecast": { + "times": raw_weather.get("hourly", {}).get("time", []), + "temperature": raw_weather.get("hourly", {}).get("temperature_2m", []), + "humidity": raw_weather.get("hourly", {}).get("relative_humidity_2m", []), + "wind_speed": raw_weather.get("hourly", {}).get("wind_speed_10m", []), + "wind_direction": raw_weather.get("hourly", {}).get("wind_direction_10m", []), + "precipitation": raw_weather.get("hourly", {}).get("precipitation", []), + "weather_code": raw_weather.get("hourly", {}).get("weather_code", []), + "visibility": raw_weather.get("hourly", {}).get("visibility", []), + "cloud_cover": raw_weather.get("hourly", {}).get("cloud_cover", []), + "pressure": raw_weather.get("hourly", {}).get("pressure_msl", []) + }, + "daily_forecast": { + "dates": raw_weather.get("daily", {}).get("time", []), + "temperature_max": raw_weather.get("daily", {}).get("temperature_2m_max", []), + "temperature_min": raw_weather.get("daily", {}).get("temperature_2m_min", []), + "sunrise": raw_weather.get("daily", {}).get("sunrise", []), + "sunset": raw_weather.get("daily", {}).get("sunset", []), + "precipitation_sum": raw_weather.get("daily", {}).get("precipitation_sum", []), + "wind_speed_max": raw_weather.get("daily", {}).get("wind_speed_10m_max", []), + "wind_direction_dominant": raw_weather.get("daily", {}).get("wind_direction_10m_dominant", []) + }, + "target_date": target_date.strftime("%Y-%m-%d"), + "data_retrieved_at": datetime.now().isoformat() + } + + return structured_data + + except Exception as e: + return {"error": f"Error structuring weather data: {e}"} + + async def extract_booking_weather_data(self, booking_data: Dict) -> Dict: + """Main extraction function - returns structured data for external use.""" + try: + # Extract all booking information + booking_info = self.extract_booking_info(booking_data) + + # Extract coordinates and target date + latitude, longitude = self.extract_coordinates(booking_data) + print(f"Latitude: {latitude}, Longitude: {longitude}") + target_date = self.parse_target_date(booking_data) + + # Fetch raw weather data + raw_weather = await self.fetch_weather_data(latitude, longitude, target_date) + + # Structure the weather data + structured_weather = self.structure_weather_data(raw_weather, target_date) + + # Return combined structured data + return { + # "booking_data": booking_info, + # "coordinates": { + # "latitude": latitude, + # "longitude": longitude + # }, + # "target_date": target_date.strftime("%Y-%m-%d"), + "weather_data": structured_weather, + "extraction_metadata": { + "processed_at": datetime.now().isoformat(), + "api_endpoint": self.base_url, + "data_source": "open-meteo" + } + } + + except Exception as e: + return { + "error": f"Data extraction failed: {e}", + "job_id": booking_data.get("job_id", "unknown"), + "processed_at": datetime.now().isoformat() + } +# Example usage +async def main(): + """Demonstrate the data extraction system.""" + + # Sample booking data + booking_form_input = { + "job_id": "1043", + "job_overview": { + "job_number": "Job #1043", + "site_name": "Hightower Solar Farm", + "assigned_date": "January 23, 2025", + "status": "Scheduled" + }, + "site_information": { + "site_name": "Hightower Solar Farm", + "region": "North England", + "full_address": "Grange Lane, Manchester M34 7TF", + "gps_coordinates": { + "latitude": "13.41° N", + "longitude": "52.52 W" + } + }, + "timing": { + "start_time": "09:00 AM", + "end_time": "10:30 AM", + "survey_duration": "30–45 mins", + "buffer_time": "45 mins" + }, + "form": { + "asset_type": "Solar Farm", + "system_size": "5.2 MW capacity, approximately 16,000 panels across 12 hectares", + "survey_purpose": "Insurance assessment", + "assigned_engineer": "David Wilson - 0161-555-0876", + "contact_person": "Sarah Thompson", + "contact_phone": "0161-555-0234" + } + } + + # Initialize extractor + extractor = DroneWeatherDataExtractor() + + # Extract structured data + result = await extractor.extract_booking_weather_data(booking_form_input) + + # Display structured output + print("=== STRUCTURED DATA EXTRACTION ===") + print(json.dumps(result, indent=2)) if __name__ == "__main__": - terminal_chat() + asyncio.run(main()) \ No newline at end of file diff --git a/test2.py b/test2.py new file mode 100644 index 0000000..ace5981 --- /dev/null +++ b/test2.py @@ -0,0 +1,48 @@ +booking_form_input = { + "job_id": "1043", + "job_overview": { + "job_number": "Job #1043", + "site_name": "Hightower Solar Farm", + "assigned_date": "January 23, 2025", + "status": "Scheduled" + }, + "site_information": { + "site_name": "Hightower Solar Farm", + "region": "North England", + "full_address": "Grange Lane, Manchester M34 7TF", + "gps_coordinates": { + "latitude": "53.4408° N", + "longitude": "2.2426° W" + } + }, + "timing": { + "start_time": "09:00 AM", + "end_time": "10:30 AM", + "survey_duration": "30–45 mins", + "buffer_time": "45 mins" + }, + "form": { + "asset_type": "Solar Farm", + "asset_details": "null", + "site_name": "Hightower Solar Farm", + "site_location": "Grange Lane, Manchester M34 7TF, GPS: 53.4408° N, 2.2426° W", + "system_size": "5.2 MW capacity, approximately 16,000 panels across 12 hectares", + "access_type": "Restricted Access", + "access_details": "Main gate access code: 7841. Contact facility manager Sarah Thompson at 0161-555-0234 for entry. Follow yellow markers to operations building. Site requires visitor registration.", + "vehicle_access": "Yes - Vehicle Accessible", + "contact_person": "Sarah Thompson", + "contact_phone": "0161-555-0234", + "contact_role": "Facility Manager", + "contact_availability": "Available on-site", + "special_safety_requirements": "Hard hats, high-vis vests, and steel-toe boots mandatory. Safety induction required (20 minutes). Keep 100m distance from substation area. Weather restriction: no flights in winds >15mph.", + "survey_purpose": "Insurance assessment", + "survey_purpose_details": "Annual insurance inspection required for policy renewal. Focus on structural integrity, panel condition, and inverter housing. Insurance company requires thermal imaging and detailed photographic documentation.", + "additional_requirements": "Morning survey preferred for optimal lighting conditions. Need detailed documentation of any panel damage or hotspots. Provide georeferenced images for insurance mapping. Weather backup date: January 24th same time.", + "timing_preference": "Within 1 week", + "preferred_dates": "January 23rd morning preferred, backup January 24th", + "selected_slot": "January 23rd, 2025 - 09:00 AM - 10:30 AM", + "assigned_engineer": "David Wilson - 0161-555-0876", + "booking_timestamp": "2025-01-15T10:45:22Z", + "booking_id": "DSV-2025-0115-007" + } +} \ No newline at end of file diff --git a/test3.py b/test3.py new file mode 100644 index 0000000..77730d5 --- /dev/null +++ b/test3.py @@ -0,0 +1,78 @@ +import os +import json +import asyncio +from langchain_openai import ChatOpenAI +from langchain_core.messages import HumanMessage, SystemMessage +from src.prompts.templates.flght_prompt import flight_prompt # You must save the prompt function here +from src.config.llm_config import LlmConfig +from config import Config +from src.components.data_extraction.weather_data import DroneWeatherDataExtractor + +class DroneAssessmentAgent: + def __init__(self): + self.llm = ChatOpenAI( + api_key=Config.OPENAI_API_KEY, + model=LlmConfig.openai.models.gpt_4o, + temperature=0.3 + ) + + self.weather_extractor = DroneWeatherDataExtractor() + + async def run(self, booking_form: str) -> dict: + """ + Run the drone environmental & safety assessment agent. + + Args: + booking_form_text (str): Raw text of the booking form + + Returns: + dict: JSON output of the AI assessment + """ + weather_data = await self.weather_extractor.extract_booking_weather_data(booking_form) + booking_form_text = json.dumps(booking_form) + + prompt = flight_prompt(booking_form_text, weather_data) # Contains the instructions + JSON template + messages = [ + SystemMessage(content=prompt), + HumanMessage(content=booking_form_text) + ] + + try: + response = self.llm.invoke(messages) + + if hasattr(response, "content"): + response_text = response.content.strip() + print("Raw LLM Response:\n", response_text[:300], "...\n") + + # Attempt to parse JSON from response + start_idx = response_text.find('{') + end_idx = response_text.rfind('}') + 1 + + if start_idx == -1 or end_idx == -1: + raise ValueError("No JSON object found in output") + + json_str = response_text[start_idx:end_idx] + return json.loads(json_str) + + else: + raise ValueError("LLM returned no usable content") + + except Exception as e: + print(f"Error in DroneAssessmentAgent: {str(e)}") + return { + "error": str(e), + "raw_response": response.content if 'response' in locals() else None + } + +async def main(): + """Async main function to properly handle the async agent.""" + from test2 import booking_form_input + + agent = DroneAssessmentAgent() + result = await agent.run(booking_form_input) + + print("\nStructured Assessment Output:\n") + print(json.dumps(result, indent=2)) + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/tests/unit/weather_data_extractor.py b/tests/unit/weather_data_extractor.py new file mode 100644 index 0000000..e69de29