Spaces:
Sleeping
Sleeping
Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,717 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app.py - FastAPI version
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import logging
|
| 5 |
+
import os
|
| 6 |
+
from typing import Dict, Any, Optional, List, Union
|
| 7 |
+
from dataclasses import dataclass, asdict
|
| 8 |
+
from contextlib import asynccontextmanager
|
| 9 |
+
import uuid
|
| 10 |
+
from datetime import datetime, timedelta
|
| 11 |
+
|
| 12 |
+
from fastapi import FastAPI, HTTPException, Depends, status, BackgroundTasks
|
| 13 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 14 |
+
from fastapi.responses import JSONResponse
|
| 15 |
+
from pydantic import BaseModel, Field, validator
|
| 16 |
+
from openai import OpenAI, AsyncOpenAI
|
| 17 |
+
from dotenv import load_dotenv
|
| 18 |
+
import asyncio
|
| 19 |
+
from cachetools import TTLCache
|
| 20 |
+
|
| 21 |
+
# Import your modules
|
| 22 |
+
from easy_agents import EASYFARMS_FUNCTION_SCHEMAS, EasyFarmsAgent, execute_easyfarms_function
|
| 23 |
+
from alert import chat_with_weather_assistant, WEATHER_TOOLS
|
| 24 |
+
|
| 25 |
+
# Configure logging
|
| 26 |
+
logging.basicConfig(level=logging.INFO)
|
| 27 |
+
logger = logging.getLogger(__name__)
|
| 28 |
+
|
| 29 |
+
# Load environment variables
|
| 30 |
+
load_dotenv()
|
| 31 |
+
|
| 32 |
+
# ===================== Configuration =====================
|
| 33 |
+
|
| 34 |
+
@dataclass
|
| 35 |
+
class Config:
|
| 36 |
+
"""Configuration settings"""
|
| 37 |
+
api_key: str
|
| 38 |
+
api_url: str
|
| 39 |
+
model_name: str
|
| 40 |
+
max_retries: int = 3
|
| 41 |
+
temperature: float = 0.7
|
| 42 |
+
|
| 43 |
+
@classmethod
|
| 44 |
+
def from_env(cls):
|
| 45 |
+
"""Load configuration from environment variables"""
|
| 46 |
+
return cls(
|
| 47 |
+
api_key=os.getenv("API_KEY"),
|
| 48 |
+
api_url=os.getenv("API_URL"),
|
| 49 |
+
model_name=os.getenv("MODEL_NAME", "gpt-4-turbo-preview")
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
# ===================== Pydantic Models =====================
|
| 53 |
+
|
| 54 |
+
class QueryRequest(BaseModel):
|
| 55 |
+
"""Request model for query endpoint"""
|
| 56 |
+
message: str = Field(..., min_length=1, max_length=5000, description="User's query message")
|
| 57 |
+
session_id: Optional[str] = Field(None, description="Session ID for conversation continuity")
|
| 58 |
+
save_history: bool = Field(True, description="Whether to save this interaction in history")
|
| 59 |
+
|
| 60 |
+
class Config:
|
| 61 |
+
schema_extra = {
|
| 62 |
+
"example": {
|
| 63 |
+
"message": "What crop should I grow with N=80, P=45, K=120, temperature 25°C, humidity 70%?",
|
| 64 |
+
"session_id": "123e4567-e89b-12d3-a456-426614174000",
|
| 65 |
+
"save_history": True
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
class QueryResponse(BaseModel):
|
| 70 |
+
"""Response model for query endpoint"""
|
| 71 |
+
response: str
|
| 72 |
+
session_id: str
|
| 73 |
+
timestamp: datetime
|
| 74 |
+
function_calls: Optional[List[Dict[str, Any]]] = None
|
| 75 |
+
|
| 76 |
+
class Config:
|
| 77 |
+
schema_extra = {
|
| 78 |
+
"example": {
|
| 79 |
+
"response": "Based on the soil conditions...",
|
| 80 |
+
"session_id": "123e4567-e89b-12d3-a456-426614174000",
|
| 81 |
+
"timestamp": "2024-01-15T10:30:00",
|
| 82 |
+
"function_calls": []
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
class CropRecommendationRequest(BaseModel):
|
| 87 |
+
"""Request model for crop recommendation"""
|
| 88 |
+
nitrogen: int = Field(..., ge=0, le=200, description="Nitrogen content (N)")
|
| 89 |
+
phosphorus: int = Field(..., ge=0, le=200, description="Phosphorus content (P)")
|
| 90 |
+
potassium: int = Field(..., ge=0, le=200, description="Potassium content (K)")
|
| 91 |
+
temperature: float = Field(..., ge=-10, le=50, description="Temperature in Celsius")
|
| 92 |
+
humidity: float = Field(..., ge=0, le=100, description="Humidity percentage")
|
| 93 |
+
ph: float = Field(6.5, ge=0, le=14, description="Soil pH value")
|
| 94 |
+
|
| 95 |
+
class Config:
|
| 96 |
+
schema_extra = {
|
| 97 |
+
"example": {
|
| 98 |
+
"nitrogen": 80,
|
| 99 |
+
"phosphorus": 45,
|
| 100 |
+
"potassium": 120,
|
| 101 |
+
"temperature": 25.0,
|
| 102 |
+
"humidity": 70.0,
|
| 103 |
+
"ph": 6.5
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
class FertilizerRecommendationRequest(BaseModel):
|
| 108 |
+
"""Request model for fertilizer recommendation"""
|
| 109 |
+
crop: str = Field(..., description="Crop type")
|
| 110 |
+
soil_type: str = Field(..., description="Type of soil")
|
| 111 |
+
nitrogen: int = Field(..., ge=0, le=200, description="Current nitrogen content")
|
| 112 |
+
phosphorus: int = Field(..., ge=0, le=200, description="Current phosphorus content")
|
| 113 |
+
potassium: int = Field(..., ge=0, le=200, description="Current potassium content")
|
| 114 |
+
temperature: Optional[float] = Field(None, description="Temperature in Celsius")
|
| 115 |
+
humidity: Optional[float] = Field(None, description="Humidity percentage")
|
| 116 |
+
moisture: Optional[float] = Field(None, description="Soil moisture percentage")
|
| 117 |
+
|
| 118 |
+
class WeatherAlertRequest(BaseModel):
|
| 119 |
+
"""Request model for weather alerts"""
|
| 120 |
+
location: Optional[str] = Field(None, description="Location for weather alert")
|
| 121 |
+
include_forecast: bool = Field(True, description="Include weather forecast")
|
| 122 |
+
|
| 123 |
+
class PlantDiseaseRequest(BaseModel):
|
| 124 |
+
"""Request model for plant disease detection"""
|
| 125 |
+
symptoms: str = Field(..., description="Description of plant symptoms")
|
| 126 |
+
crop_type: Optional[str] = Field(None, description="Type of crop affected")
|
| 127 |
+
image_url: Optional[str] = Field(None, description="URL to plant image")
|
| 128 |
+
|
| 129 |
+
class SessionInfo(BaseModel):
|
| 130 |
+
"""Session information"""
|
| 131 |
+
session_id: str
|
| 132 |
+
created_at: datetime
|
| 133 |
+
last_activity: datetime
|
| 134 |
+
message_count: int
|
| 135 |
+
|
| 136 |
+
class HealthResponse(BaseModel):
|
| 137 |
+
"""Health check response"""
|
| 138 |
+
status: str
|
| 139 |
+
version: str
|
| 140 |
+
timestamp: datetime
|
| 141 |
+
|
| 142 |
+
# ===================== Session Manager =====================
|
| 143 |
+
|
| 144 |
+
class SessionManager:
|
| 145 |
+
"""Manages user sessions and conversation history"""
|
| 146 |
+
|
| 147 |
+
def __init__(self, ttl_hours: int = 24):
|
| 148 |
+
self.sessions: Dict[str, Dict[str, Any]] = {}
|
| 149 |
+
self.ttl = timedelta(hours=ttl_hours)
|
| 150 |
+
|
| 151 |
+
def create_session(self) -> str:
|
| 152 |
+
"""Create a new session"""
|
| 153 |
+
session_id = str(uuid.uuid4())
|
| 154 |
+
self.sessions[session_id] = {
|
| 155 |
+
"id": session_id,
|
| 156 |
+
"created_at": datetime.now(),
|
| 157 |
+
"last_activity": datetime.now(),
|
| 158 |
+
"history": [],
|
| 159 |
+
"message_count": 0
|
| 160 |
+
}
|
| 161 |
+
return session_id
|
| 162 |
+
|
| 163 |
+
def get_session(self, session_id: str) -> Optional[Dict[str, Any]]:
|
| 164 |
+
"""Get session by ID"""
|
| 165 |
+
if session_id in self.sessions:
|
| 166 |
+
session = self.sessions[session_id]
|
| 167 |
+
# Check if session has expired
|
| 168 |
+
if datetime.now() - session["last_activity"] > self.ttl:
|
| 169 |
+
del self.sessions[session_id]
|
| 170 |
+
return None
|
| 171 |
+
session["last_activity"] = datetime.now()
|
| 172 |
+
return session
|
| 173 |
+
return None
|
| 174 |
+
|
| 175 |
+
def update_session(self, session_id: str, history_entry: Dict[str, Any]):
|
| 176 |
+
"""Update session history"""
|
| 177 |
+
if session_id in self.sessions:
|
| 178 |
+
self.sessions[session_id]["history"].append(history_entry)
|
| 179 |
+
self.sessions[session_id]["message_count"] += 1
|
| 180 |
+
self.sessions[session_id]["last_activity"] = datetime.now()
|
| 181 |
+
|
| 182 |
+
def clear_session(self, session_id: str):
|
| 183 |
+
"""Clear session history"""
|
| 184 |
+
if session_id in self.sessions:
|
| 185 |
+
self.sessions[session_id]["history"] = []
|
| 186 |
+
self.sessions[session_id]["message_count"] = 0
|
| 187 |
+
|
| 188 |
+
def cleanup_expired_sessions(self):
|
| 189 |
+
"""Remove expired sessions"""
|
| 190 |
+
current_time = datetime.now()
|
| 191 |
+
expired = [
|
| 192 |
+
sid for sid, session in self.sessions.items()
|
| 193 |
+
if current_time - session["last_activity"] > self.ttl
|
| 194 |
+
]
|
| 195 |
+
for sid in expired:
|
| 196 |
+
del self.sessions[sid]
|
| 197 |
+
return len(expired)
|
| 198 |
+
|
| 199 |
+
# ===================== EasyFarms Assistant =====================
|
| 200 |
+
|
| 201 |
+
class EasyFarmsAssistant:
|
| 202 |
+
"""Enhanced EasyFarms AI Assistant with async support"""
|
| 203 |
+
|
| 204 |
+
def __init__(self, config: Optional[Config] = None):
|
| 205 |
+
"""Initialize the assistant with configuration"""
|
| 206 |
+
self.config = config or Config.from_env()
|
| 207 |
+
|
| 208 |
+
# Async client for FastAPI
|
| 209 |
+
self.async_client = AsyncOpenAI(
|
| 210 |
+
api_key=self.config.api_key,
|
| 211 |
+
base_url=self.config.api_url
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
# Sync client for backward compatibility
|
| 215 |
+
self.sync_client = OpenAI(
|
| 216 |
+
api_key=self.config.api_key,
|
| 217 |
+
base_url=self.config.api_url
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
# Initialize tools
|
| 221 |
+
self.tools = self._initialize_tools()
|
| 222 |
+
|
| 223 |
+
# System prompts
|
| 224 |
+
self.system_prompt = """
|
| 225 |
+
You are the HAl AI assistant for EasyForms Agritech Solutions. Your task is to provide users with clear, concise, and actionable responses regarding agriculture, crop management, production, treatment, weather alerts, and related queries.
|
| 226 |
+
|
| 227 |
+
Core Capabilities:
|
| 228 |
+
- Crop recommendations based on soil and weather conditions
|
| 229 |
+
- Fertilizer recommendations for specific crops
|
| 230 |
+
- Plant disease detection and treatment advice
|
| 231 |
+
- Weather alerts and forecasts for farming decisions
|
| 232 |
+
- General agricultural guidance
|
| 233 |
+
|
| 234 |
+
Rules:
|
| 235 |
+
1. Check if any relevant function_tools or datasets are available for this query.
|
| 236 |
+
2. If available, use the functions to fetch information and generate the final user-facing response.
|
| 237 |
+
3. If the functions or data are unavailable, do not stop; instead, generate a general, well-reasoned response based on your own knowledge.
|
| 238 |
+
4. Keep the response simple, smooth, well-pointed, and concise.
|
| 239 |
+
5. Structure the response with bullet points or numbered steps where helpful.
|
| 240 |
+
6. Provide practical, actionable advice a user can implement immediately.
|
| 241 |
+
7. Use English or Hindi based on user preference.
|
| 242 |
+
8. If any information is uncertain, mention it clearly and suggest alternatives.
|
| 243 |
+
9. For weather-related queries, prioritize safety and timely alerts.
|
| 244 |
+
"""
|
| 245 |
+
|
| 246 |
+
self.final_system = """
|
| 247 |
+
You are the final response assistant for EasyForms Agritech Solutions and your name is HAL AI. Use the outputs from previous function calls to generate a clear, concise, actionable response for the user.
|
| 248 |
+
|
| 249 |
+
Rules:
|
| 250 |
+
1. Combine the function outputs and your own reasoning to answer the query.
|
| 251 |
+
2. Keep responses simple, smooth, well-pointed, and concise.
|
| 252 |
+
3. Structure response with headings or bullet points if helpful.
|
| 253 |
+
4. Provide practical advice that a farmer or user can implement immediately.
|
| 254 |
+
5. If some data is missing, clearly state it and offer alternatives.
|
| 255 |
+
6. Use English or Hindi based on the user preference.
|
| 256 |
+
7. For weather alerts, emphasize urgency and protective measures.
|
| 257 |
+
"""
|
| 258 |
+
|
| 259 |
+
# Cache for function results
|
| 260 |
+
self.cache = TTLCache(maxsize=100, ttl=300) # 5 minute cache
|
| 261 |
+
|
| 262 |
+
def _initialize_tools(self) -> List[Dict]:
|
| 263 |
+
"""Initialize and convert function schemas to new tools format"""
|
| 264 |
+
tools = []
|
| 265 |
+
|
| 266 |
+
# Convert EasyFarms schemas
|
| 267 |
+
for schema in EASYFARMS_FUNCTION_SCHEMAS:
|
| 268 |
+
tool = {
|
| 269 |
+
"type": "function",
|
| 270 |
+
"function": {
|
| 271 |
+
"name": schema["name"],
|
| 272 |
+
"description": schema["description"],
|
| 273 |
+
"parameters": schema["parameters"]
|
| 274 |
+
}
|
| 275 |
+
}
|
| 276 |
+
tools.append(tool)
|
| 277 |
+
|
| 278 |
+
# Add weather tools
|
| 279 |
+
tools.extend(WEATHER_TOOLS)
|
| 280 |
+
|
| 281 |
+
return tools
|
| 282 |
+
|
| 283 |
+
async def call_function_async(self, function_name: str, arguments: Dict) -> Any:
|
| 284 |
+
"""Async version of function calling"""
|
| 285 |
+
try:
|
| 286 |
+
# Check cache first
|
| 287 |
+
cache_key = f"{function_name}:{json.dumps(arguments, sort_keys=True)}"
|
| 288 |
+
if cache_key in self.cache:
|
| 289 |
+
logger.info(f"Cache hit for {function_name}")
|
| 290 |
+
return self.cache[cache_key]
|
| 291 |
+
|
| 292 |
+
# Execute function in thread pool to avoid blocking
|
| 293 |
+
loop = asyncio.get_event_loop()
|
| 294 |
+
|
| 295 |
+
if function_name.startswith("get_weather"):
|
| 296 |
+
result = await loop.run_in_executor(
|
| 297 |
+
None,
|
| 298 |
+
self._execute_weather_function,
|
| 299 |
+
function_name,
|
| 300 |
+
arguments
|
| 301 |
+
)
|
| 302 |
+
else:
|
| 303 |
+
result = await loop.run_in_executor(
|
| 304 |
+
None,
|
| 305 |
+
execute_easyfarms_function,
|
| 306 |
+
function_name,
|
| 307 |
+
arguments
|
| 308 |
+
)
|
| 309 |
+
|
| 310 |
+
# Cache result
|
| 311 |
+
self.cache[cache_key] = result
|
| 312 |
+
return result
|
| 313 |
+
|
| 314 |
+
except Exception as e:
|
| 315 |
+
logger.error(f"Error executing function {function_name}: {e}")
|
| 316 |
+
return {"error": str(e)}
|
| 317 |
+
|
| 318 |
+
def _execute_weather_function(self, function_name: str, kwargs: Dict):
|
| 319 |
+
"""Execute weather functions"""
|
| 320 |
+
from alert import execute_function
|
| 321 |
+
return execute_function(function_name, kwargs)
|
| 322 |
+
|
| 323 |
+
async def process_query_async(
|
| 324 |
+
self,
|
| 325 |
+
user_message: str,
|
| 326 |
+
conversation_history: List[Dict[str, Any]] = None
|
| 327 |
+
) -> Dict[str, Any]:
|
| 328 |
+
"""Process user query asynchronously"""
|
| 329 |
+
try:
|
| 330 |
+
messages = [
|
| 331 |
+
{"role": "system", "content": self.system_prompt},
|
| 332 |
+
{"role": "user", "content": user_message}
|
| 333 |
+
]
|
| 334 |
+
|
| 335 |
+
# Add conversation history if exists
|
| 336 |
+
if conversation_history:
|
| 337 |
+
# Include last 5 interactions for context
|
| 338 |
+
messages = [messages[0]] + conversation_history[-10:] + [messages[1]]
|
| 339 |
+
|
| 340 |
+
# First API call
|
| 341 |
+
response = await self.async_client.chat.completions.create(
|
| 342 |
+
model=self.config.model_name,
|
| 343 |
+
messages=messages,
|
| 344 |
+
tools=self.tools,
|
| 345 |
+
tool_choice="auto",
|
| 346 |
+
temperature=self.config.temperature
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
message = response.choices[0].message
|
| 350 |
+
function_calls_made = []
|
| 351 |
+
|
| 352 |
+
# Check if tool needs to be called
|
| 353 |
+
if hasattr(message, 'tool_calls') and message.tool_calls:
|
| 354 |
+
messages.append(message)
|
| 355 |
+
|
| 356 |
+
# Execute all tool calls
|
| 357 |
+
for tool_call in message.tool_calls:
|
| 358 |
+
function_name = tool_call.function.name
|
| 359 |
+
function_args = json.loads(tool_call.function.arguments)
|
| 360 |
+
|
| 361 |
+
logger.info(f"Calling function: {function_name} with args: {function_args}")
|
| 362 |
+
|
| 363 |
+
# Call function asynchronously
|
| 364 |
+
function_result = await self.call_function_async(function_name, function_args)
|
| 365 |
+
|
| 366 |
+
function_calls_made.append({
|
| 367 |
+
"name": function_name,
|
| 368 |
+
"arguments": function_args,
|
| 369 |
+
"result": function_result
|
| 370 |
+
})
|
| 371 |
+
|
| 372 |
+
# Add function result to messages
|
| 373 |
+
messages.append({
|
| 374 |
+
"role": "tool",
|
| 375 |
+
"tool_call_id": tool_call.id,
|
| 376 |
+
"content": json.dumps(function_result)
|
| 377 |
+
})
|
| 378 |
+
|
| 379 |
+
# Add final system prompt
|
| 380 |
+
messages.append({
|
| 381 |
+
"role": "system",
|
| 382 |
+
"content": self.final_system
|
| 383 |
+
})
|
| 384 |
+
|
| 385 |
+
# Get final response
|
| 386 |
+
final_response = await self.async_client.chat.completions.create(
|
| 387 |
+
model=self.config.model_name,
|
| 388 |
+
messages=messages,
|
| 389 |
+
temperature=self.config.temperature
|
| 390 |
+
)
|
| 391 |
+
|
| 392 |
+
response_content = final_response.choices[0].message.content
|
| 393 |
+
else:
|
| 394 |
+
response_content = message.content
|
| 395 |
+
|
| 396 |
+
return {
|
| 397 |
+
"response": response_content,
|
| 398 |
+
"function_calls": function_calls_made
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
except Exception as e:
|
| 402 |
+
logger.error(f"Error processing query: {e}")
|
| 403 |
+
raise HTTPException(
|
| 404 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 405 |
+
detail=f"Error processing query: {str(e)}"
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
+
# ===================== FastAPI App =====================
|
| 409 |
+
|
| 410 |
+
# App lifespan manager
|
| 411 |
+
@asynccontextmanager
|
| 412 |
+
async def lifespan(app: FastAPI):
|
| 413 |
+
"""Manage app lifecycle"""
|
| 414 |
+
# Startup
|
| 415 |
+
logger.info("Starting EasyFarms API...")
|
| 416 |
+
app.state.assistant = EasyFarmsAssistant()
|
| 417 |
+
app.state.session_manager = SessionManager()
|
| 418 |
+
|
| 419 |
+
# Background task to cleanup sessions
|
| 420 |
+
async def cleanup_sessions():
|
| 421 |
+
while True:
|
| 422 |
+
await asyncio.sleep(3600) # Run every hour
|
| 423 |
+
expired = app.state.session_manager.cleanup_expired_sessions()
|
| 424 |
+
if expired > 0:
|
| 425 |
+
logger.info(f"Cleaned up {expired} expired sessions")
|
| 426 |
+
|
| 427 |
+
# Start background task
|
| 428 |
+
cleanup_task = asyncio.create_task(cleanup_sessions())
|
| 429 |
+
|
| 430 |
+
yield
|
| 431 |
+
|
| 432 |
+
# Shutdown
|
| 433 |
+
cleanup_task.cancel()
|
| 434 |
+
logger.info("Shutting down EasyFarms API...")
|
| 435 |
+
|
| 436 |
+
# Create FastAPI app
|
| 437 |
+
app = FastAPI(
|
| 438 |
+
title="EasyFarms Agricultural Assistant API",
|
| 439 |
+
description="AI-powered agricultural assistant for crop management, fertilizer recommendations, and weather alerts",
|
| 440 |
+
version="1.0.0",
|
| 441 |
+
lifespan=lifespan
|
| 442 |
+
)
|
| 443 |
+
|
| 444 |
+
# Add CORS middleware
|
| 445 |
+
app.add_middleware(
|
| 446 |
+
CORSMiddleware,
|
| 447 |
+
allow_origins=["*"], # Configure appropriately for production
|
| 448 |
+
allow_credentials=True,
|
| 449 |
+
allow_methods=["*"],
|
| 450 |
+
allow_headers=["*"],
|
| 451 |
+
)
|
| 452 |
+
|
| 453 |
+
# ===================== Dependencies =====================
|
| 454 |
+
|
| 455 |
+
def get_assistant() -> EasyFarmsAssistant:
|
| 456 |
+
"""Get assistant instance"""
|
| 457 |
+
return app.state.assistant
|
| 458 |
+
|
| 459 |
+
def get_session_manager() -> SessionManager:
|
| 460 |
+
"""Get session manager instance"""
|
| 461 |
+
return app.state.session_manager
|
| 462 |
+
|
| 463 |
+
# ===================== API Endpoints =====================
|
| 464 |
+
|
| 465 |
+
@app.get("/", response_model=HealthResponse)
|
| 466 |
+
async def root():
|
| 467 |
+
"""Root endpoint - health check"""
|
| 468 |
+
return HealthResponse(
|
| 469 |
+
status="healthy",
|
| 470 |
+
version="1.0.0",
|
| 471 |
+
timestamp=datetime.now()
|
| 472 |
+
)
|
| 473 |
+
|
| 474 |
+
@app.get("/health", response_model=HealthResponse)
|
| 475 |
+
async def health_check():
|
| 476 |
+
"""Health check endpoint"""
|
| 477 |
+
return HealthResponse(
|
| 478 |
+
status="healthy",
|
| 479 |
+
version="1.0.0",
|
| 480 |
+
timestamp=datetime.now()
|
| 481 |
+
)
|
| 482 |
+
|
| 483 |
+
@app.post("/api/query", response_model=QueryResponse)
|
| 484 |
+
async def process_query(
|
| 485 |
+
request: QueryRequest,
|
| 486 |
+
assistant: EasyFarmsAssistant = Depends(get_assistant),
|
| 487 |
+
session_manager: SessionManager = Depends(get_session_manager)
|
| 488 |
+
):
|
| 489 |
+
"""
|
| 490 |
+
Process a general query to the agricultural assistant
|
| 491 |
+
"""
|
| 492 |
+
# Handle session
|
| 493 |
+
if request.session_id:
|
| 494 |
+
session = session_manager.get_session(request.session_id)
|
| 495 |
+
if not session:
|
| 496 |
+
request.session_id = session_manager.create_session()
|
| 497 |
+
session = session_manager.get_session(request.session_id)
|
| 498 |
+
else:
|
| 499 |
+
request.session_id = session_manager.create_session()
|
| 500 |
+
session = session_manager.get_session(request.session_id)
|
| 501 |
+
|
| 502 |
+
# Process query
|
| 503 |
+
result = await assistant.process_query_async(
|
| 504 |
+
request.message,
|
| 505 |
+
conversation_history=session["history"] if session else None
|
| 506 |
+
)
|
| 507 |
+
|
| 508 |
+
# Update session history if requested
|
| 509 |
+
if request.save_history and session:
|
| 510 |
+
session_manager.update_session(
|
| 511 |
+
request.session_id,
|
| 512 |
+
{"role": "user", "content": request.message}
|
| 513 |
+
)
|
| 514 |
+
session_manager.update_session(
|
| 515 |
+
request.session_id,
|
| 516 |
+
{"role": "assistant", "content": result["response"]}
|
| 517 |
+
)
|
| 518 |
+
|
| 519 |
+
return QueryResponse(
|
| 520 |
+
response=result["response"],
|
| 521 |
+
session_id=request.session_id,
|
| 522 |
+
timestamp=datetime.now(),
|
| 523 |
+
function_calls=result.get("function_calls")
|
| 524 |
+
)
|
| 525 |
+
|
| 526 |
+
@app.post("/api/crop-recommendation")
|
| 527 |
+
async def get_crop_recommendation(
|
| 528 |
+
request: CropRecommendationRequest,
|
| 529 |
+
assistant: EasyFarmsAssistant = Depends(get_assistant)
|
| 530 |
+
):
|
| 531 |
+
"""
|
| 532 |
+
Get crop recommendation based on soil and weather conditions
|
| 533 |
+
"""
|
| 534 |
+
query = f"What crop should I grow with N={request.nitrogen}, P={request.phosphorus}, K={request.potassium}, temperature {request.temperature}°C, humidity {request.humidity}%, pH {request.ph}?"
|
| 535 |
+
|
| 536 |
+
result = await assistant.process_query_async(query)
|
| 537 |
+
|
| 538 |
+
return {
|
| 539 |
+
"recommendation": result["response"],
|
| 540 |
+
"input_parameters": request.dict(),
|
| 541 |
+
"timestamp": datetime.now()
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
@app.post("/api/fertilizer-recommendation")
|
| 545 |
+
async def get_fertilizer_recommendation(
|
| 546 |
+
request: FertilizerRecommendationRequest,
|
| 547 |
+
assistant: EasyFarmsAssistant = Depends(get_assistant)
|
| 548 |
+
):
|
| 549 |
+
"""
|
| 550 |
+
Get fertilizer recommendation for specific crop and soil conditions
|
| 551 |
+
"""
|
| 552 |
+
query_parts = [
|
| 553 |
+
f"I need fertilizer recommendation for {request.crop} in {request.soil_type} soil",
|
| 554 |
+
f"with N={request.nitrogen}, P={request.phosphorus}, K={request.potassium}"
|
| 555 |
+
]
|
| 556 |
+
|
| 557 |
+
if request.temperature:
|
| 558 |
+
query_parts.append(f"temperature {request.temperature}°C")
|
| 559 |
+
if request.humidity:
|
| 560 |
+
query_parts.append(f"humidity {request.humidity}%")
|
| 561 |
+
if request.moisture:
|
| 562 |
+
query_parts.append(f"moisture {request.moisture}%")
|
| 563 |
+
|
| 564 |
+
query = ", ".join(query_parts)
|
| 565 |
+
result = await assistant.process_query_async(query)
|
| 566 |
+
|
| 567 |
+
return {
|
| 568 |
+
"recommendation": result["response"],
|
| 569 |
+
"crop": request.crop,
|
| 570 |
+
"soil_type": request.soil_type,
|
| 571 |
+
"timestamp": datetime.now()
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
@app.post("/api/weather-alert")
|
| 575 |
+
async def get_weather_alert(
|
| 576 |
+
request: WeatherAlertRequest,
|
| 577 |
+
assistant: EasyFarmsAssistant = Depends(get_assistant)
|
| 578 |
+
):
|
| 579 |
+
"""
|
| 580 |
+
Get weather alerts for farming
|
| 581 |
+
"""
|
| 582 |
+
location_str = f" for {request.location}" if request.location else ""
|
| 583 |
+
query = f"What are the current weather alerts and conditions{location_str}? How will this affect farming?"
|
| 584 |
+
|
| 585 |
+
if request.include_forecast:
|
| 586 |
+
query += " Include the weather forecast."
|
| 587 |
+
|
| 588 |
+
result = await assistant.process_query_async(query)
|
| 589 |
+
|
| 590 |
+
return {
|
| 591 |
+
"alerts": result["response"],
|
| 592 |
+
"location": request.location or "Default location",
|
| 593 |
+
"timestamp": datetime.now()
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
@app.post("/api/plant-disease")
|
| 597 |
+
async def detect_plant_disease(
|
| 598 |
+
request: PlantDiseaseRequest,
|
| 599 |
+
assistant: EasyFarmsAssistant = Depends(get_assistant)
|
| 600 |
+
):
|
| 601 |
+
"""
|
| 602 |
+
Detect plant disease based on symptoms
|
| 603 |
+
"""
|
| 604 |
+
query_parts = [f"My plants have these symptoms: {request.symptoms}"]
|
| 605 |
+
|
| 606 |
+
if request.crop_type:
|
| 607 |
+
query_parts.append(f"The crop is {request.crop_type}.")
|
| 608 |
+
|
| 609 |
+
query_parts.append("What could be the problem and how should I treat it?")
|
| 610 |
+
|
| 611 |
+
query = " ".join(query_parts)
|
| 612 |
+
result = await assistant.process_query_async(query)
|
| 613 |
+
|
| 614 |
+
return {
|
| 615 |
+
"diagnosis": result["response"],
|
| 616 |
+
"symptoms": request.symptoms,
|
| 617 |
+
"crop_type": request.crop_type,
|
| 618 |
+
"timestamp": datetime.now()
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
@app.get("/api/session/{session_id}")
|
| 622 |
+
async def get_session_info(
|
| 623 |
+
session_id: str,
|
| 624 |
+
session_manager: SessionManager = Depends(get_session_manager)
|
| 625 |
+
):
|
| 626 |
+
"""
|
| 627 |
+
Get information about a specific session
|
| 628 |
+
"""
|
| 629 |
+
session = session_manager.get_session(session_id)
|
| 630 |
+
|
| 631 |
+
if not session:
|
| 632 |
+
raise HTTPException(
|
| 633 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 634 |
+
detail="Session not found or expired"
|
| 635 |
+
)
|
| 636 |
+
|
| 637 |
+
return SessionInfo(
|
| 638 |
+
session_id=session["id"],
|
| 639 |
+
created_at=session["created_at"],
|
| 640 |
+
last_activity=session["last_activity"],
|
| 641 |
+
message_count=session["message_count"]
|
| 642 |
+
)
|
| 643 |
+
|
| 644 |
+
@app.delete("/api/session/{session_id}")
|
| 645 |
+
async def clear_session(
|
| 646 |
+
session_id: str,
|
| 647 |
+
session_manager: SessionManager = Depends(get_session_manager)
|
| 648 |
+
):
|
| 649 |
+
"""
|
| 650 |
+
Clear session history
|
| 651 |
+
"""
|
| 652 |
+
session = session_manager.get_session(session_id)
|
| 653 |
+
|
| 654 |
+
if not session:
|
| 655 |
+
raise HTTPException(
|
| 656 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 657 |
+
detail="Session not found or expired"
|
| 658 |
+
)
|
| 659 |
+
|
| 660 |
+
session_manager.clear_session(session_id)
|
| 661 |
+
|
| 662 |
+
return {"message": "Session history cleared", "session_id": session_id}
|
| 663 |
+
|
| 664 |
+
@app.get("/api/supported-options")
|
| 665 |
+
async def get_supported_options(
|
| 666 |
+
assistant: EasyFarmsAssistant = Depends(get_assistant)
|
| 667 |
+
):
|
| 668 |
+
"""
|
| 669 |
+
Get all supported options for crops, fertilizers, etc.
|
| 670 |
+
"""
|
| 671 |
+
result = await assistant.process_query_async("Show me all supported crop types and options")
|
| 672 |
+
|
| 673 |
+
return {
|
| 674 |
+
"options": result["response"],
|
| 675 |
+
"timestamp": datetime.now()
|
| 676 |
+
}
|
| 677 |
+
|
| 678 |
+
# ===================== Error Handlers =====================
|
| 679 |
+
|
| 680 |
+
@app.exception_handler(HTTPException)
|
| 681 |
+
async def http_exception_handler(request, exc):
|
| 682 |
+
"""Handle HTTP exceptions"""
|
| 683 |
+
return JSONResponse(
|
| 684 |
+
status_code=exc.status_code,
|
| 685 |
+
content={
|
| 686 |
+
"error": exc.detail,
|
| 687 |
+
"status_code": exc.status_code,
|
| 688 |
+
"timestamp": datetime.now().isoformat()
|
| 689 |
+
}
|
| 690 |
+
)
|
| 691 |
+
|
| 692 |
+
@app.exception_handler(Exception)
|
| 693 |
+
async def general_exception_handler(request, exc):
|
| 694 |
+
"""Handle general exceptions"""
|
| 695 |
+
logger.error(f"Unhandled exception: {exc}")
|
| 696 |
+
return JSONResponse(
|
| 697 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 698 |
+
content={
|
| 699 |
+
"error": "Internal server error",
|
| 700 |
+
"status_code": 500,
|
| 701 |
+
"timestamp": datetime.now().isoformat()
|
| 702 |
+
}
|
| 703 |
+
)
|
| 704 |
+
|
| 705 |
+
# ===================== Main =====================
|
| 706 |
+
|
| 707 |
+
if __name__ == "__main__":
|
| 708 |
+
import uvicorn
|
| 709 |
+
|
| 710 |
+
# Run the FastAPI app
|
| 711 |
+
uvicorn.run(
|
| 712 |
+
"app:app",
|
| 713 |
+
host="0.0.0.0",
|
| 714 |
+
port=7860,
|
| 715 |
+
reload=True,
|
| 716 |
+
log_level="info"
|
| 717 |
+
)
|