Spaces:
Sleeping
Sleeping
Upload 10 files
Browse files- app.py +327 -717
- conversation_manager.py +70 -0
- easy_agents.py +745 -307
- functions_calling.py +61 -0
- server.py +139 -0
- static/script.js +233 -0
- static/style.css +164 -0
- templates/index.html +60 -0
app.py
CHANGED
|
@@ -1,717 +1,327 @@
|
|
| 1 |
-
# app.py
|
| 2 |
-
|
| 3 |
-
import json
|
| 4 |
-
import
|
| 5 |
-
import
|
| 6 |
-
from
|
| 7 |
-
|
| 8 |
-
from
|
| 9 |
-
import
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
from
|
| 14 |
-
from
|
| 15 |
-
from
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
#
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
for
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
"""
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
"
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 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 |
-
)
|
|
|
|
| 1 |
+
# app.py
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import openai
|
| 5 |
+
from typing import Dict, Any, Optional, List
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
import logging
|
| 8 |
+
from openai import OpenAI
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
import os
|
| 11 |
+
|
| 12 |
+
# Import your modules
|
| 13 |
+
from easy_agents import EASYFARMS_FUNCTION_SCHEMAS, execute_easyfarms_function
|
| 14 |
+
from alert import WEATHER_TOOLS , execute_function
|
| 15 |
+
from conversation_manager import ConversationManager
|
| 16 |
+
|
| 17 |
+
# Configure logging
|
| 18 |
+
logging.basicConfig(level=logging.INFO)
|
| 19 |
+
logger = logging.getLogger(__name__)
|
| 20 |
+
|
| 21 |
+
# Load environment variables
|
| 22 |
+
load_dotenv()
|
| 23 |
+
|
| 24 |
+
@dataclass
|
| 25 |
+
class Config:
|
| 26 |
+
"""Configuration settings"""
|
| 27 |
+
api_key: str
|
| 28 |
+
api_url: str
|
| 29 |
+
model_name: str
|
| 30 |
+
max_retries: int = 3
|
| 31 |
+
temperature: float = 0.5
|
| 32 |
+
|
| 33 |
+
@classmethod
|
| 34 |
+
def from_env(cls):
|
| 35 |
+
"""Load configuration from environment variables"""
|
| 36 |
+
return cls(
|
| 37 |
+
api_key=os.getenv("API_KEY"),
|
| 38 |
+
api_url=os.getenv("API_URL"),
|
| 39 |
+
model_name=os.getenv("MODEL_NAME")
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
class EasyFarmsAssistant:
|
| 43 |
+
"""Enhanced EasyFarms AI Assistant with weather integration and persistent sessions"""
|
| 44 |
+
|
| 45 |
+
def __init__(self, config: Optional[Config] = None, manager: Optional[ConversationManager] = None):
|
| 46 |
+
"""
|
| 47 |
+
Initialize the assistant with configuration and a conversation manager.
|
| 48 |
+
|
| 49 |
+
Args:
|
| 50 |
+
config (Optional[Config]): Configuration object. If None, loads from environment.
|
| 51 |
+
manager (Optional[ConversationManager]): Manager for handling conversation persistence.
|
| 52 |
+
"""
|
| 53 |
+
self.config = config or Config.from_env()
|
| 54 |
+
|
| 55 |
+
# Validate configuration
|
| 56 |
+
if not all([self.config.api_key, self.config.api_url, self.config.model_name]):
|
| 57 |
+
raise ValueError("Missing required configuration: API_KEY, API_URL, and MODEL_NAME must be set")
|
| 58 |
+
|
| 59 |
+
self.client = OpenAI(
|
| 60 |
+
api_key=self.config.api_key,
|
| 61 |
+
base_url=self.config.api_url
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
# All available functions from both modules are combined into the tools list
|
| 65 |
+
self.tools = self._initialize_tools()
|
| 66 |
+
|
| 67 |
+
# Use the provided conversation manager or create a new one
|
| 68 |
+
self.manager = manager or ConversationManager()
|
| 69 |
+
|
| 70 |
+
# System prompts
|
| 71 |
+
self.system_prompt = """You are the 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.
|
| 72 |
+
|
| 73 |
+
Core Capabilities:
|
| 74 |
+
- Crop recommendations based on soil and weather conditions
|
| 75 |
+
- Fertilizer recommendations for specific crops
|
| 76 |
+
- Plant disease detection and treatment advice
|
| 77 |
+
- Weather alerts and forecasts for farming decisions
|
| 78 |
+
- Market data and commodity prices
|
| 79 |
+
- General agricultural guidance
|
| 80 |
+
|
| 81 |
+
Rules:
|
| 82 |
+
1. Check if any relevant function_tools or datasets are available for this query.
|
| 83 |
+
2. If available, use the functions to fetch information and generate the final user-facing response.
|
| 84 |
+
3. If the functions or data are unavailable, do **not stop**; instead, generate a general, well-reasoned response based on your own knowledge.
|
| 85 |
+
4. Keep the response **simple, smooth, well-pointed, and concise**.
|
| 86 |
+
5. Structure the response with bullet points or numbered steps where helpful.
|
| 87 |
+
6. Provide practical, actionable advice a user can implement immediately.
|
| 88 |
+
7. Use English or Hindi based on user preference.
|
| 89 |
+
8. If any information is uncertain, mention it clearly and suggest alternatives.
|
| 90 |
+
9. For weather-related queries, prioritize safety and timely alerts."""
|
| 91 |
+
|
| 92 |
+
self.final_system = """You are the final response assistant for EasyForms Agritech Solutions. Use the outputs from previous function calls to generate a **clear, concise, actionable response** for the user.
|
| 93 |
+
|
| 94 |
+
Rules:
|
| 95 |
+
1. Combine the function outputs and your own reasoning to answer the query.
|
| 96 |
+
2. Keep responses simple, smooth, well-pointed, and concise.
|
| 97 |
+
3. Structure response with headings or bullet points if helpful.
|
| 98 |
+
4. Provide practical advice that a farmer or user can implement immediately.
|
| 99 |
+
5. If some data is missing, clearly state it and offer alternatives.
|
| 100 |
+
6. Use English or Hindi based on the user preference.
|
| 101 |
+
7. For weather alerts, emphasize urgency and protective measures."""
|
| 102 |
+
|
| 103 |
+
def _initialize_tools(self) -> List[Dict]:
|
| 104 |
+
"""Initialize and convert all function schemas to the new tools format"""
|
| 105 |
+
tools = []
|
| 106 |
+
|
| 107 |
+
# Convert EasyFarms schemas to the new format
|
| 108 |
+
for schema in EASYFARMS_FUNCTION_SCHEMAS:
|
| 109 |
+
tool = {
|
| 110 |
+
"type": "function",
|
| 111 |
+
"function": {
|
| 112 |
+
"name": schema["name"],
|
| 113 |
+
"description": schema["description"],
|
| 114 |
+
"parameters": schema["parameters"]
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
tools.append(tool)
|
| 118 |
+
|
| 119 |
+
# Add weather tools (which are already in the correct format)
|
| 120 |
+
tools.extend(WEATHER_TOOLS)
|
| 121 |
+
|
| 122 |
+
return tools
|
| 123 |
+
|
| 124 |
+
def call_function(self, function_name: str, arguments: Dict) -> Any:
|
| 125 |
+
"""Route function calls to appropriate handlers with error handling"""
|
| 126 |
+
try:
|
| 127 |
+
# Map all available function names to their handlers
|
| 128 |
+
function_map = {
|
| 129 |
+
# EasyFarms functions
|
| 130 |
+
"get_crop_recommendation": lambda args: execute_easyfarms_function("get_crop_recommendation", **args),
|
| 131 |
+
"get_fertilizer_recommendation": lambda args: execute_easyfarms_function("get_fertilizer_recommendation", **args),
|
| 132 |
+
"detect_plant_disease": lambda args: execute_easyfarms_function("detect_plant_disease", **args),
|
| 133 |
+
"get_supported_options": lambda args: execute_easyfarms_function("get_supported_options", **args),
|
| 134 |
+
"get_market_prices": lambda args: execute_easyfarms_function("get_market_prices", **args),
|
| 135 |
+
"compare_commodity_prices": lambda args: execute_easyfarms_function("compare_commodity_prices", **args),
|
| 136 |
+
"get_market_locations": lambda args: execute_easyfarms_function("get_market_locations", **args),
|
| 137 |
+
"get_commodity_list": lambda args: execute_easyfarms_function("get_commodity_list", **args),
|
| 138 |
+
|
| 139 |
+
# Weather alert functions
|
| 140 |
+
"get_weather_alerts": lambda args: self._execute_weather_function("get_weather_alerts", **args),
|
| 141 |
+
"get_weather": lambda args: self._execute_weather_function("get_weather", **args),
|
| 142 |
+
"get_alert_summary": lambda args: self._execute_weather_function("get_alert_summary", **args),
|
| 143 |
+
"get_available_locations": lambda args: self._execute_weather_function("get_available_locations", **args)
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
if function_name in function_map:
|
| 147 |
+
return function_map[function_name](arguments)
|
| 148 |
+
else:
|
| 149 |
+
return {"error": f"Unknown function: {function_name}"}
|
| 150 |
+
|
| 151 |
+
except Exception as e:
|
| 152 |
+
logger.error(f"Error executing function {function_name}: {e}")
|
| 153 |
+
return {"error": str(e)}
|
| 154 |
+
|
| 155 |
+
def _execute_weather_function(self, function_name: str, **kwargs):
|
| 156 |
+
"""Helper to execute weather functions from the alert.py module"""
|
| 157 |
+
from alert import execute_function
|
| 158 |
+
return execute_function(function_name, kwargs)
|
| 159 |
+
|
| 160 |
+
def process_query(self, user_message: str, session_id: str, image_url: Optional[str] = None) -> str:
|
| 161 |
+
"""
|
| 162 |
+
Process user query, correctly reformatting history for the LLM API call.
|
| 163 |
+
"""
|
| 164 |
+
try:
|
| 165 |
+
# MEMORY STEP 1: Fetch the complete past conversation using the session_id.
|
| 166 |
+
conversation_history = self.manager.get_history(session_id)
|
| 167 |
+
|
| 168 |
+
# Prepare the list that will be sent to the AI
|
| 169 |
+
messages = [{"role": "system", "content": self.system_prompt}]
|
| 170 |
+
|
| 171 |
+
# MEMORY STEP 2: Loop through the history and add every past message.
|
| 172 |
+
# This builds the AI's memory of what was said before.
|
| 173 |
+
for message in conversation_history:
|
| 174 |
+
if message.get("role") == "user":
|
| 175 |
+
llm_user_content = message.get("content", "")
|
| 176 |
+
if message.get("imageUrl"):
|
| 177 |
+
llm_user_content += f" [image_url: {message.get('imageUrl')}]"
|
| 178 |
+
messages.append({"role": "user", "content": llm_user_content})
|
| 179 |
+
elif message.get("role") == "assistant":
|
| 180 |
+
messages.append({"role": "assistant", "content": message.get("content", "")})
|
| 181 |
+
|
| 182 |
+
# MEMORY STEP 3: Add the user's CURRENT message to the end of the history.
|
| 183 |
+
llm_message_content = user_message
|
| 184 |
+
if image_url:
|
| 185 |
+
llm_message_content += f" [image_url: {image_url}]"
|
| 186 |
+
messages.append({"role": "user", "content": llm_message_content})
|
| 187 |
+
|
| 188 |
+
# MEMORY STEP 4: Send the entire 'messages' list to the AI.
|
| 189 |
+
response = self.client.chat.completions.create(
|
| 190 |
+
model=self.config.model_name,
|
| 191 |
+
messages=messages,
|
| 192 |
+
tools=self.tools,
|
| 193 |
+
tool_choice="auto",
|
| 194 |
+
temperature=self.config.temperature
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
message = response.choices[0].message
|
| 198 |
+
|
| 199 |
+
if hasattr(message, 'tool_calls') and message.tool_calls:
|
| 200 |
+
# Add the assistant's message with tool calls
|
| 201 |
+
messages.append({
|
| 202 |
+
"role": "assistant",
|
| 203 |
+
"tool_calls": [
|
| 204 |
+
{
|
| 205 |
+
"id": tool_call.id,
|
| 206 |
+
"type": "function",
|
| 207 |
+
"function": {
|
| 208 |
+
"name": tool_call.function.name,
|
| 209 |
+
"arguments": tool_call.function.arguments
|
| 210 |
+
}
|
| 211 |
+
} for tool_call in message.tool_calls
|
| 212 |
+
]
|
| 213 |
+
})
|
| 214 |
+
|
| 215 |
+
# Execute all tool calls
|
| 216 |
+
for tool_call in message.tool_calls:
|
| 217 |
+
function_name = tool_call.function.name
|
| 218 |
+
function_args = json.loads(tool_call.function.arguments)
|
| 219 |
+
|
| 220 |
+
logger.info(f"Calling function: {function_name} with args: {function_args}")
|
| 221 |
+
|
| 222 |
+
# Call the function
|
| 223 |
+
function_result = self.call_function(function_name, function_args)
|
| 224 |
+
|
| 225 |
+
# Add function result to messages
|
| 226 |
+
messages.append({
|
| 227 |
+
"role": "tool",
|
| 228 |
+
"tool_call_id": tool_call.id,
|
| 229 |
+
"content": json.dumps(function_result)
|
| 230 |
+
})
|
| 231 |
+
|
| 232 |
+
# Add final system prompt for generating response
|
| 233 |
+
messages.append({
|
| 234 |
+
"role": "system",
|
| 235 |
+
"content": self.final_system
|
| 236 |
+
})
|
| 237 |
+
|
| 238 |
+
# Get final response
|
| 239 |
+
final_response = self.client.chat.completions.create(
|
| 240 |
+
model=self.config.model_name,
|
| 241 |
+
messages=messages,
|
| 242 |
+
temperature=self.config.temperature
|
| 243 |
+
)
|
| 244 |
+
response_content = final_response.choices[0].message.content
|
| 245 |
+
else:
|
| 246 |
+
response_content = message.content
|
| 247 |
+
|
| 248 |
+
# After getting the response, save the new turns back to the database for the next message.
|
| 249 |
+
user_turn_for_storage = {"role": "user", "content": user_message}
|
| 250 |
+
if image_url:
|
| 251 |
+
user_turn_for_storage["imageUrl"] = image_url
|
| 252 |
+
|
| 253 |
+
updated_history = conversation_history + [
|
| 254 |
+
user_turn_for_storage,
|
| 255 |
+
{"role": "assistant", "content": response_content}
|
| 256 |
+
]
|
| 257 |
+
self.manager.save_history(session_id, updated_history)
|
| 258 |
+
|
| 259 |
+
return response_content
|
| 260 |
+
|
| 261 |
+
except Exception as e:
|
| 262 |
+
logger.error(f"Error processing query for session {session_id}: {e}")
|
| 263 |
+
return f"I apologize, but I encountered an error: {str(e)}. Please try again or rephrase your question."
|
| 264 |
+
|
| 265 |
+
def clear_history(self, session_id: str) -> bool:
|
| 266 |
+
"""
|
| 267 |
+
Clear conversation history for a specific session from the database.
|
| 268 |
+
|
| 269 |
+
Args:
|
| 270 |
+
session_id: The ID of the session to clear.
|
| 271 |
+
|
| 272 |
+
Returns:
|
| 273 |
+
True if deletion was successful, False otherwise.
|
| 274 |
+
"""
|
| 275 |
+
logger.info(f"Clearing history for session: {session_id}")
|
| 276 |
+
return self.manager.delete_history(session_id)
|
| 277 |
+
|
| 278 |
+
# Utility class for generating example queries (can be used for testing)
|
| 279 |
+
class QuickQueries:
|
| 280 |
+
"""Pre-defined query templates for common farming questions"""
|
| 281 |
+
|
| 282 |
+
@staticmethod
|
| 283 |
+
def crop_recommendation(N: int, P: int, K: int, temp: float, humidity: float, ph: float = 6.5) -> str:
|
| 284 |
+
"""Generate crop recommendation query"""
|
| 285 |
+
return f"What crop should I grow with N={N}, P={P}, K={K}, temperature {temp}°C, humidity {humidity}%, pH {ph}?"
|
| 286 |
+
|
| 287 |
+
@staticmethod
|
| 288 |
+
def fertilizer_query(crop: str, soil: str, N: int, P: int, K: int) -> str:
|
| 289 |
+
"""Generate fertilizer recommendation query"""
|
| 290 |
+
return f"I need fertilizer recommendation for {crop} in {soil} soil with N={N}, P={P}, K={K}"
|
| 291 |
+
|
| 292 |
+
@staticmethod
|
| 293 |
+
def weather_alert(location: str = "") -> str:
|
| 294 |
+
"""Generate weather alert query"""
|
| 295 |
+
location_str = f" for {location}" if location else ""
|
| 296 |
+
return f"What are the current weather alerts and conditions{location_str}? How will this affect farming?"
|
| 297 |
+
|
| 298 |
+
# Test function to validate configuration
|
| 299 |
+
def test_configuration():
|
| 300 |
+
"""Test if all configuration is properly set up"""
|
| 301 |
+
try:
|
| 302 |
+
# Check environment variables
|
| 303 |
+
required_env_vars = ["API_KEY", "API_URL", "MODEL_NAME"]
|
| 304 |
+
missing_vars = [var for var in required_env_vars if not os.getenv(var)]
|
| 305 |
+
|
| 306 |
+
if missing_vars:
|
| 307 |
+
print(f"❌ Missing environment variables: {missing_vars}")
|
| 308 |
+
return False
|
| 309 |
+
|
| 310 |
+
# Test assistant initialization
|
| 311 |
+
assistant = EasyFarmsAssistant()
|
| 312 |
+
print("✅ Assistant initialized successfully")
|
| 313 |
+
|
| 314 |
+
# Test function schemas
|
| 315 |
+
print(f"✅ Loaded {len(assistant.tools)} function tools")
|
| 316 |
+
|
| 317 |
+
return True
|
| 318 |
+
except Exception as e:
|
| 319 |
+
print(f"❌ Configuration test failed: {e}")
|
| 320 |
+
return False
|
| 321 |
+
|
| 322 |
+
if __name__ == "__main__":
|
| 323 |
+
print("=== EasyFarms Assistant Configuration Test ===")
|
| 324 |
+
if test_configuration():
|
| 325 |
+
print("✅ All systems ready!")
|
| 326 |
+
else:
|
| 327 |
+
print("❌ Please fix configuration issues before running the assistant.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
conversation_manager.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# conversation_manager.py
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
from supabase import create_client, Client
|
| 5 |
+
from typing import List, Dict, Any, Optional
|
| 6 |
+
import logging
|
| 7 |
+
|
| 8 |
+
logger = logging.getLogger(__name__)
|
| 9 |
+
|
| 10 |
+
class ConversationManager:
|
| 11 |
+
def __init__(self):
|
| 12 |
+
"""Initializes the Supabase client."""
|
| 13 |
+
supabase_url = os.getenv("SUPABASE_URL")
|
| 14 |
+
supabase_key = os.getenv("SUPABASE_KEY")
|
| 15 |
+
|
| 16 |
+
if not supabase_url or not supabase_key:
|
| 17 |
+
raise ValueError("Supabase URL and Key must be set in environment variables.")
|
| 18 |
+
|
| 19 |
+
self.supabase: Client = create_client(supabase_url, supabase_key)
|
| 20 |
+
self.table_name = "conversations"
|
| 21 |
+
logger.info("ConversationManager initialized with Supabase client.")
|
| 22 |
+
|
| 23 |
+
def get_history(self, session_id: str) -> List[Dict[str, Any]]:
|
| 24 |
+
"""
|
| 25 |
+
Retrieves conversation history for a given session_id.
|
| 26 |
+
Returns an empty list if no history is found.
|
| 27 |
+
"""
|
| 28 |
+
try:
|
| 29 |
+
response = self.supabase.table(self.table_name)\
|
| 30 |
+
.select("history")\
|
| 31 |
+
.eq("session_id", session_id)\
|
| 32 |
+
.limit(1)\
|
| 33 |
+
.execute()
|
| 34 |
+
|
| 35 |
+
if response.data:
|
| 36 |
+
return response.data[0].get("history", [])
|
| 37 |
+
return []
|
| 38 |
+
except Exception as e:
|
| 39 |
+
logger.error(f"Error fetching history for session {session_id}: {e}")
|
| 40 |
+
return []
|
| 41 |
+
|
| 42 |
+
def save_history(self, session_id: str, history: List[Dict[str, Any]]) -> None:
|
| 43 |
+
"""
|
| 44 |
+
Saves or updates the conversation history for a session_id.
|
| 45 |
+
Uses 'upsert' to create a new record or update an existing one.
|
| 46 |
+
"""
|
| 47 |
+
try:
|
| 48 |
+
self.supabase.table(self.table_name).upsert({
|
| 49 |
+
"session_id": session_id,
|
| 50 |
+
"history": history
|
| 51 |
+
}).execute()
|
| 52 |
+
logger.info(f"History for session {session_id} saved successfully.")
|
| 53 |
+
except Exception as e:
|
| 54 |
+
logger.error(f"Error saving history for session {session_id}: {e}")
|
| 55 |
+
|
| 56 |
+
def delete_history(self, session_id: str) -> bool:
|
| 57 |
+
"""
|
| 58 |
+
Deletes the conversation history for a given session_id.
|
| 59 |
+
Returns True on success, False on failure.
|
| 60 |
+
"""
|
| 61 |
+
try:
|
| 62 |
+
self.supabase.table(self.table_name)\
|
| 63 |
+
.delete()\
|
| 64 |
+
.eq("session_id", session_id)\
|
| 65 |
+
.execute()
|
| 66 |
+
logger.info(f"History for session {session_id} deleted successfully.")
|
| 67 |
+
return True
|
| 68 |
+
except Exception as e:
|
| 69 |
+
logger.error(f"Error deleting history for session {session_id}: {e}")
|
| 70 |
+
return False
|
easy_agents.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
# easy_agents.py
|
| 2 |
|
| 3 |
import json
|
| 4 |
import requests
|
|
@@ -6,6 +6,9 @@ import logging
|
|
| 6 |
from typing import Optional, Dict, Any, Union, List
|
| 7 |
from functools import lru_cache
|
| 8 |
import time
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
# Configure logging
|
| 11 |
logging.basicConfig(level=logging.INFO)
|
|
@@ -14,6 +17,7 @@ logger = logging.getLogger(__name__)
|
|
| 14 |
class EasyFarmsAgent:
|
| 15 |
"""
|
| 16 |
EasyFarms AI Agent optimized for function calling in AI systems.
|
|
|
|
| 17 |
"""
|
| 18 |
|
| 19 |
def __init__(self, timeout: int = 30, max_retries: int = 3):
|
|
@@ -28,8 +32,17 @@ class EasyFarmsAgent:
|
|
| 28 |
'Accept': 'application/json'
|
| 29 |
})
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
# Load mappings
|
| 32 |
self._load_mappings()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
def _load_mappings(self):
|
| 35 |
"""Pre-load all mappings for faster access."""
|
|
@@ -154,15 +167,610 @@ class EasyFarmsAgent:
|
|
| 154 |
'message': f'Error parsing HTML response: {str(e)}',
|
| 155 |
'raw_html': html_content[:500] + '...'
|
| 156 |
}
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
|
| 161 |
# =============================================================================
|
| 162 |
-
# FUNCTION
|
| 163 |
# =============================================================================
|
| 164 |
|
| 165 |
-
# Function schemas for AI agent function calling
|
| 166 |
EASYFARMS_FUNCTION_SCHEMAS = [
|
| 167 |
{
|
| 168 |
"name": "get_crop_recommendation",
|
|
@@ -286,7 +894,7 @@ EASYFARMS_FUNCTION_SCHEMAS = [
|
|
| 286 |
},
|
| 287 |
"image_path": {
|
| 288 |
"type": "string",
|
| 289 |
-
"description": "Path to the plant/leaf image file"
|
| 290 |
},
|
| 291 |
"language": {
|
| 292 |
"type": "string",
|
|
@@ -298,6 +906,94 @@ EASYFARMS_FUNCTION_SCHEMAS = [
|
|
| 298 |
"required": ["crop", "image_path"]
|
| 299 |
}
|
| 300 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
{
|
| 302 |
"name": "get_supported_options",
|
| 303 |
"description": "Get lists of supported crops and soil types for different analysis modes. Useful for showing available options to users.",
|
|
@@ -315,276 +1011,19 @@ EASYFARMS_FUNCTION_SCHEMAS = [
|
|
| 315 |
}
|
| 316 |
]
|
| 317 |
|
| 318 |
-
# =============================================================================
|
| 319 |
-
# FUNCTION IMPLEMENTATIONS
|
| 320 |
-
# =============================================================================
|
| 321 |
-
|
| 322 |
-
def get_crop_recommendation(
|
| 323 |
-
N: int,
|
| 324 |
-
P: int,
|
| 325 |
-
K: int,
|
| 326 |
-
temperature: float,
|
| 327 |
-
humidity: float,
|
| 328 |
-
ph: float = None,
|
| 329 |
-
rainfall: float = 100
|
| 330 |
-
) -> Dict[str, Any]:
|
| 331 |
-
try:
|
| 332 |
-
data = {
|
| 333 |
-
"N": N, "P": P, "K": K,
|
| 334 |
-
"temperature": temperature,
|
| 335 |
-
"humidity": humidity,
|
| 336 |
-
"ph": ph if ph is not None else 6.5,
|
| 337 |
-
"rainfall": rainfall
|
| 338 |
-
}
|
| 339 |
-
|
| 340 |
-
# The response will be parsed by _make_request_with_retry
|
| 341 |
-
response = _easyfarms_agent._make_request_with_retry(
|
| 342 |
-
"POST",
|
| 343 |
-
_easyfarms_agent.endpoints["crop"],
|
| 344 |
-
data=data
|
| 345 |
-
)
|
| 346 |
-
|
| 347 |
-
# Add input parameters to the response
|
| 348 |
-
if isinstance(response, dict):
|
| 349 |
-
response['input_parameters'] = data
|
| 350 |
-
if 'mode' not in response:
|
| 351 |
-
response['mode'] = 'crop_recommendation'
|
| 352 |
-
|
| 353 |
-
logger.info(f"Crop recommendation response: {response}")
|
| 354 |
-
return response
|
| 355 |
-
|
| 356 |
-
except Exception as e:
|
| 357 |
-
error_result = {
|
| 358 |
-
"error": str(e),
|
| 359 |
-
"status": "error",
|
| 360 |
-
"mode": "crop_recommendation"
|
| 361 |
-
}
|
| 362 |
-
logger.error(f"Crop recommendation failed: {e}")
|
| 363 |
-
return error_result
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
def get_fertilizer_recommendation(
|
| 367 |
-
crop: str,
|
| 368 |
-
soil: str,
|
| 369 |
-
temperature: float,
|
| 370 |
-
humidity: float,
|
| 371 |
-
moisture: float,
|
| 372 |
-
N: int,
|
| 373 |
-
P: int,
|
| 374 |
-
K: int
|
| 375 |
-
) -> Dict[str, Any]:
|
| 376 |
-
try:
|
| 377 |
-
# Map soil and crop to codes
|
| 378 |
-
soil_code = _easyfarms_agent.soil_mapping.get(soil.lower(), soil)
|
| 379 |
-
crop_code = _easyfarms_agent.fertilizer_crop_mapping.get(crop.lower(), crop)
|
| 380 |
-
|
| 381 |
-
data = {
|
| 382 |
-
"temperature": temperature,
|
| 383 |
-
"humidity": humidity,
|
| 384 |
-
"moisture": moisture,
|
| 385 |
-
"N": N, "P": P, "K": K,
|
| 386 |
-
"soil": soil_code,
|
| 387 |
-
"crop": crop_code
|
| 388 |
-
}
|
| 389 |
-
|
| 390 |
-
# The response will be parsed by _make_request_with_retry
|
| 391 |
-
response = _easyfarms_agent._make_request_with_retry(
|
| 392 |
-
"POST",
|
| 393 |
-
_easyfarms_agent.endpoints["fertilizer"],
|
| 394 |
-
data=data
|
| 395 |
-
)
|
| 396 |
-
|
| 397 |
-
# Add input parameters to the response
|
| 398 |
-
if isinstance(response, dict):
|
| 399 |
-
response['input_parameters'] = {
|
| 400 |
-
**data,
|
| 401 |
-
"original_soil": soil,
|
| 402 |
-
"original_crop": crop
|
| 403 |
-
}
|
| 404 |
-
if 'mode' not in response:
|
| 405 |
-
response['mode'] = 'fertilizer_recommendation'
|
| 406 |
-
|
| 407 |
-
logger.info(f"Fertilizer recommendation response: {response}")
|
| 408 |
-
return response
|
| 409 |
-
|
| 410 |
-
except Exception as e:
|
| 411 |
-
error_result = {
|
| 412 |
-
"status": "error",
|
| 413 |
-
"error": str(e),
|
| 414 |
-
"mode": "fertilizer_recommendation"
|
| 415 |
-
}
|
| 416 |
-
logger.error(f"Fertilizer recommendation failed: {e}")
|
| 417 |
-
return error_result
|
| 418 |
-
|
| 419 |
-
def detect_plant_disease(
|
| 420 |
-
crop: str,
|
| 421 |
-
image_path: str,
|
| 422 |
-
language: str = "en"
|
| 423 |
-
) -> Dict[str, Any]:
|
| 424 |
-
"""
|
| 425 |
-
Detect plant diseases from an image using the exact API format from curl.
|
| 426 |
-
|
| 427 |
-
Args:
|
| 428 |
-
crop: Type of crop/plant in the image
|
| 429 |
-
image_path: Path to the plant/leaf image file
|
| 430 |
-
language: Response language code (default: 'en')
|
| 431 |
-
|
| 432 |
-
Returns:
|
| 433 |
-
Dictionary with disease detection results and metadata
|
| 434 |
-
"""
|
| 435 |
-
try:
|
| 436 |
-
# Map crop to disease API format
|
| 437 |
-
crop_key = _easyfarms_agent.disease_crop_mapping.get(crop.lower(), crop.lower())
|
| 438 |
-
|
| 439 |
-
# Check if file exists
|
| 440 |
-
import os
|
| 441 |
-
if not os.path.exists(image_path):
|
| 442 |
-
return {
|
| 443 |
-
"error": f"Image file not found: {image_path}",
|
| 444 |
-
"status": "error",
|
| 445 |
-
"mode": "disease_detection"
|
| 446 |
-
}
|
| 447 |
-
|
| 448 |
-
# Prepare headers exactly like the curl command
|
| 449 |
-
headers = {
|
| 450 |
-
'accept': '*/*',
|
| 451 |
-
'accept-language': 'en-IN,en-US;q=0.9,en;q=0.8,hi;q=0.7',
|
| 452 |
-
'origin': 'https://app.easyfarms.in',
|
| 453 |
-
'referer': 'https://app.easyfarms.in/',
|
| 454 |
-
'sec-ch-ua': '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"',
|
| 455 |
-
'sec-ch-ua-mobile': '?1',
|
| 456 |
-
'sec-ch-ua-platform': '"Android"',
|
| 457 |
-
'sec-fetch-dest': 'empty',
|
| 458 |
-
'sec-fetch-mode': 'cors',
|
| 459 |
-
'sec-fetch-site': 'cross-site',
|
| 460 |
-
'user-agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36'
|
| 461 |
-
}
|
| 462 |
-
|
| 463 |
-
# Make request with image using the exact format
|
| 464 |
-
with open(image_path, "rb") as f:
|
| 465 |
-
files = {
|
| 466 |
-
'image': ('blob', f, 'image/jpeg')
|
| 467 |
-
}
|
| 468 |
-
data = {
|
| 469 |
-
'crop': crop_key,
|
| 470 |
-
'language': language
|
| 471 |
-
}
|
| 472 |
-
|
| 473 |
-
# Use direct requests instead of the agent's retry method for this specific API
|
| 474 |
-
response = _easyfarms_agent.session.post(
|
| 475 |
-
_easyfarms_agent.endpoints["disease"],
|
| 476 |
-
files=files,
|
| 477 |
-
data=data,
|
| 478 |
-
headers=headers,
|
| 479 |
-
timeout=_easyfarms_agent.timeout
|
| 480 |
-
)
|
| 481 |
-
response.raise_for_status()
|
| 482 |
-
|
| 483 |
-
# Try to parse JSON response
|
| 484 |
-
try:
|
| 485 |
-
result = response.json()
|
| 486 |
-
except ValueError:
|
| 487 |
-
# If JSON parsing fails, return the raw text
|
| 488 |
-
result = {
|
| 489 |
-
"raw_response": response.text,
|
| 490 |
-
"content_type": response.headers.get('content-type', ''),
|
| 491 |
-
"status_code": response.status_code
|
| 492 |
-
}
|
| 493 |
-
|
| 494 |
-
# Add metadata to result
|
| 495 |
-
if isinstance(result, dict):
|
| 496 |
-
result.update({
|
| 497 |
-
"status": "success",
|
| 498 |
-
"mode": "disease_detection",
|
| 499 |
-
"input_parameters": {
|
| 500 |
-
"crop": crop,
|
| 501 |
-
"crop_key": crop_key,
|
| 502 |
-
"language": language,
|
| 503 |
-
"image_path": image_path
|
| 504 |
-
}
|
| 505 |
-
})
|
| 506 |
-
else:
|
| 507 |
-
# If result is not a dict, wrap it
|
| 508 |
-
result = {
|
| 509 |
-
"detection_result": result,
|
| 510 |
-
"status": "success",
|
| 511 |
-
"mode": "disease_detection",
|
| 512 |
-
"input_parameters": {
|
| 513 |
-
"crop": crop,
|
| 514 |
-
"crop_key": crop_key,
|
| 515 |
-
"language": language,
|
| 516 |
-
"image_path": image_path
|
| 517 |
-
}
|
| 518 |
-
}
|
| 519 |
-
|
| 520 |
-
logger.info(f"Disease detection successful for {crop}")
|
| 521 |
-
return result
|
| 522 |
-
|
| 523 |
-
except Exception as e:
|
| 524 |
-
error_result = {
|
| 525 |
-
"error": str(e),
|
| 526 |
-
"status": "error",
|
| 527 |
-
"mode": "disease_detection",
|
| 528 |
-
"input_parameters": {
|
| 529 |
-
"crop": crop,
|
| 530 |
-
"language": language,
|
| 531 |
-
"image_path": image_path
|
| 532 |
-
}
|
| 533 |
-
}
|
| 534 |
-
logger.error(f"Disease detection failed: {e}")
|
| 535 |
-
return error_result
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
def get_supported_options(mode: str) -> Dict[str, Any]:
|
| 539 |
-
"""
|
| 540 |
-
Get lists of supported options for different modes.
|
| 541 |
-
|
| 542 |
-
Args:
|
| 543 |
-
mode: Mode to get options for ("fertilizer_crops", "disease_crops", "soil_types", "all")
|
| 544 |
-
|
| 545 |
-
Returns:
|
| 546 |
-
Dictionary with supported options
|
| 547 |
-
"""
|
| 548 |
-
try:
|
| 549 |
-
result = {"status": "success", "mode": "supported_options"}
|
| 550 |
-
|
| 551 |
-
if mode == "fertilizer_crops":
|
| 552 |
-
result["fertilizer_crops"] = list(_easyfarms_agent.fertilizer_crop_mapping.keys())
|
| 553 |
-
elif mode == "disease_crops":
|
| 554 |
-
result["disease_crops"] = list(_easyfarms_agent.disease_crop_mapping.keys())
|
| 555 |
-
elif mode == "soil_types":
|
| 556 |
-
result["soil_types"] = list(_easyfarms_agent.soil_mapping.keys())
|
| 557 |
-
elif mode == "all":
|
| 558 |
-
result.update({
|
| 559 |
-
"fertilizer_crops": list(_easyfarms_agent.fertilizer_crop_mapping.keys()),
|
| 560 |
-
"disease_crops": list(_easyfarms_agent.disease_crop_mapping.keys()),
|
| 561 |
-
"soil_types": list(_easyfarms_agent.soil_mapping.keys())
|
| 562 |
-
})
|
| 563 |
-
else:
|
| 564 |
-
return {
|
| 565 |
-
"error": f"Invalid mode: {mode}. Use 'fertilizer_crops', 'disease_crops', 'soil_types', or 'all'",
|
| 566 |
-
"status": "error"
|
| 567 |
-
}
|
| 568 |
-
|
| 569 |
-
return result
|
| 570 |
-
|
| 571 |
-
except Exception as e:
|
| 572 |
-
return {
|
| 573 |
-
"error": str(e),
|
| 574 |
-
"status": "error",
|
| 575 |
-
"mode": "supported_options"
|
| 576 |
-
}
|
| 577 |
-
|
| 578 |
# =============================================================================
|
| 579 |
# FUNCTION MAPPING FOR AI AGENTS
|
| 580 |
# =============================================================================
|
| 581 |
|
| 582 |
-
# Map function names to implementations
|
| 583 |
EASYFARMS_FUNCTIONS = {
|
| 584 |
"get_crop_recommendation": get_crop_recommendation,
|
| 585 |
"get_fertilizer_recommendation": get_fertilizer_recommendation,
|
| 586 |
"detect_plant_disease": detect_plant_disease,
|
| 587 |
-
"get_supported_options": get_supported_options
|
|
|
|
|
|
|
|
|
|
|
|
|
| 588 |
}
|
| 589 |
|
| 590 |
# =============================================================================
|
|
@@ -634,18 +1073,45 @@ def get_function_names() -> List[str]:
|
|
| 634 |
# =============================================================================
|
| 635 |
|
| 636 |
if __name__ == "__main__":
|
| 637 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 638 |
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
|
|
|
|
|
|
|
|
|
| 645 |
|
| 646 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 647 |
|
| 648 |
-
# Crop recommendation
|
|
|
|
| 649 |
crop_result = execute_easyfarms_function(
|
| 650 |
"get_crop_recommendation",
|
| 651 |
N=90, P=42, K=43,
|
|
@@ -654,32 +1120,4 @@ if __name__ == "__main__":
|
|
| 654 |
ph=6.5,
|
| 655 |
rainfall=202.9
|
| 656 |
)
|
| 657 |
-
print(
|
| 658 |
-
|
| 659 |
-
# Get supported options
|
| 660 |
-
options_result = execute_easyfarms_function(
|
| 661 |
-
"get_supported_options",
|
| 662 |
-
mode="all"
|
| 663 |
-
)
|
| 664 |
-
print("Supported options:", json.dumps(options_result, indent=2))
|
| 665 |
-
|
| 666 |
-
# Fertilizer recommendation
|
| 667 |
-
fertilizer_result = execute_easyfarms_function(
|
| 668 |
-
"get_fertilizer_recommendation",
|
| 669 |
-
crop="paddy",
|
| 670 |
-
soil="loamy",
|
| 671 |
-
temperature=26.0,
|
| 672 |
-
humidity=52.0,
|
| 673 |
-
moisture=38.0,
|
| 674 |
-
N=37, P=0, K=0
|
| 675 |
-
)
|
| 676 |
-
print("Fertilizer recommendation:", json.dumps(fertilizer_result, indent=2))
|
| 677 |
-
|
| 678 |
-
# Test disease detection (if you have an image file)
|
| 679 |
-
disease_result = execute_easyfarms_function(
|
| 680 |
-
"detect_plant_disease",
|
| 681 |
-
crop="potato",
|
| 682 |
-
image_path="potato-diseases.jpg",
|
| 683 |
-
language="en"
|
| 684 |
-
)
|
| 685 |
-
print("Disease detection:", json.dumps(disease_result, indent=2))
|
|
|
|
| 1 |
+
# easy_agents.py - Enhanced with eNAM Market Data Integration
|
| 2 |
|
| 3 |
import json
|
| 4 |
import requests
|
|
|
|
| 6 |
from typing import Optional, Dict, Any, Union, List
|
| 7 |
from functools import lru_cache
|
| 8 |
import time
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
import os
|
| 11 |
+
import tempfile
|
| 12 |
|
| 13 |
# Configure logging
|
| 14 |
logging.basicConfig(level=logging.INFO)
|
|
|
|
| 17 |
class EasyFarmsAgent:
|
| 18 |
"""
|
| 19 |
EasyFarms AI Agent optimized for function calling in AI systems.
|
| 20 |
+
Now includes eNAM market data integration.
|
| 21 |
"""
|
| 22 |
|
| 23 |
def __init__(self, timeout: int = 30, max_retries: int = 3):
|
|
|
|
| 32 |
'Accept': 'application/json'
|
| 33 |
})
|
| 34 |
|
| 35 |
+
# eNAM API configuration
|
| 36 |
+
self.enam_api_key = os.environ.get('ENAM_API_KEY','579b464db66ec23bdd00000117ba747cbf6948354c9afae09a8a5087')
|
| 37 |
+
self.enam_base_url = "https://api.data.gov.in/resource/9ef84268-d588-465a-a308-a864a43d0070"
|
| 38 |
+
|
| 39 |
# Load mappings
|
| 40 |
self._load_mappings()
|
| 41 |
+
|
| 42 |
+
# Cache for market data
|
| 43 |
+
self._market_cache = {}
|
| 44 |
+
self._cache_timestamp = {}
|
| 45 |
+
self._cache_ttl = 300 # 5 minutes cache
|
| 46 |
|
| 47 |
def _load_mappings(self):
|
| 48 |
"""Pre-load all mappings for faster access."""
|
|
|
|
| 167 |
'message': f'Error parsing HTML response: {str(e)}',
|
| 168 |
'raw_html': html_content[:500] + '...'
|
| 169 |
}
|
| 170 |
+
|
| 171 |
+
def _is_cache_valid(self, cache_key: str) -> bool:
|
| 172 |
+
"""Check if cached data is still valid."""
|
| 173 |
+
if cache_key not in self._cache_timestamp:
|
| 174 |
+
return False
|
| 175 |
+
elapsed = time.time() - self._cache_timestamp[cache_key]
|
| 176 |
+
return elapsed < self._cache_ttl
|
| 177 |
+
|
| 178 |
+
def _fetch_enam_data(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
| 179 |
+
"""Fetch data from eNAM API with caching."""
|
| 180 |
+
cache_key = json.dumps(params, sort_keys=True)
|
| 181 |
+
|
| 182 |
+
# Check cache
|
| 183 |
+
if cache_key in self._market_cache and self._is_cache_valid(cache_key):
|
| 184 |
+
logger.info("Returning cached market data")
|
| 185 |
+
return self._market_cache[cache_key]
|
| 186 |
+
|
| 187 |
+
# Fetch fresh data
|
| 188 |
+
headers = {
|
| 189 |
+
'Accept': '*/*',
|
| 190 |
+
'Accept-Language': 'en-IN,en-US;q=0.9,en;q=0.8,hi;q=0.7',
|
| 191 |
+
'Connection': 'keep-alive',
|
| 192 |
+
'Origin': 'https://app.easyfarms.in',
|
| 193 |
+
'Referer': 'https://app.easyfarms.in/',
|
| 194 |
+
'Sec-Fetch-Dest': 'empty',
|
| 195 |
+
'Sec-Fetch-Mode': 'cors',
|
| 196 |
+
'Sec-Fetch-Site': 'cross-site',
|
| 197 |
+
'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36'
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
response = self._make_request_with_retry(
|
| 201 |
+
"GET",
|
| 202 |
+
self.enam_base_url,
|
| 203 |
+
params=params,
|
| 204 |
+
headers=headers
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
# Cache the response
|
| 208 |
+
self._market_cache[cache_key] = response
|
| 209 |
+
self._cache_timestamp[cache_key] = time.time()
|
| 210 |
+
|
| 211 |
+
return response
|
| 212 |
+
|
| 213 |
+
# Global agent instance
|
| 214 |
+
_easyfarms_agent = EasyFarmsAgent()
|
| 215 |
+
|
| 216 |
+
# =============================================================================
|
| 217 |
+
# MARKET DATA FUNCTION DEFINITIONS
|
| 218 |
+
# =============================================================================
|
| 219 |
+
|
| 220 |
+
def get_market_prices(
|
| 221 |
+
commodity: Optional[str] = None,
|
| 222 |
+
state: Optional[str] = None,
|
| 223 |
+
district: Optional[str] = None,
|
| 224 |
+
market: Optional[str] = None,
|
| 225 |
+
limit: int = 25
|
| 226 |
+
) -> Dict[str, Any]:
|
| 227 |
+
"""
|
| 228 |
+
Get current market prices for agricultural commodities from Indian mandis.
|
| 229 |
+
|
| 230 |
+
Args:
|
| 231 |
+
commodity: Name of the commodity (e.g., "Tomato", "Wheat", "Rice")
|
| 232 |
+
state: State name (e.g., "Gujarat", "Maharashtra")
|
| 233 |
+
district: District name
|
| 234 |
+
market: Market/mandi name
|
| 235 |
+
limit: Number of records to fetch (default: 25, max: 100)
|
| 236 |
+
|
| 237 |
+
Returns:
|
| 238 |
+
Dictionary with market price data and statistics
|
| 239 |
+
"""
|
| 240 |
+
try:
|
| 241 |
+
params = {
|
| 242 |
+
'api-key': _easyfarms_agent.enam_api_key,
|
| 243 |
+
'format': 'json',
|
| 244 |
+
'limit': min(limit, 100),
|
| 245 |
+
'offset': 0
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
# Add filters if provided
|
| 249 |
+
filters = []
|
| 250 |
+
if commodity:
|
| 251 |
+
filters.append(f"commodity:{commodity}")
|
| 252 |
+
if state:
|
| 253 |
+
filters.append(f"state:{state}")
|
| 254 |
+
if district:
|
| 255 |
+
filters.append(f"district:{district}")
|
| 256 |
+
if market:
|
| 257 |
+
filters.append(f"market:{market}")
|
| 258 |
+
|
| 259 |
+
if filters:
|
| 260 |
+
params['filters'] = ','.join(filters)
|
| 261 |
+
|
| 262 |
+
# Fetch data from eNAM API
|
| 263 |
+
response = _easyfarms_agent._fetch_enam_data(params)
|
| 264 |
+
|
| 265 |
+
records = response.get('records', [])
|
| 266 |
+
|
| 267 |
+
# Process and analyze the data
|
| 268 |
+
if records:
|
| 269 |
+
# Calculate statistics
|
| 270 |
+
prices = {
|
| 271 |
+
'min_prices': [],
|
| 272 |
+
'max_prices': [],
|
| 273 |
+
'modal_prices': []
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
for record in records:
|
| 277 |
+
try:
|
| 278 |
+
if 'min_price' in record and record['min_price']:
|
| 279 |
+
prices['min_prices'].append(float(record['min_price']))
|
| 280 |
+
if 'max_price' in record and record['max_price']:
|
| 281 |
+
prices['max_prices'].append(float(record['max_price']))
|
| 282 |
+
if 'modal_price' in record and record['modal_price']:
|
| 283 |
+
prices['modal_prices'].append(float(record['modal_price']))
|
| 284 |
+
except (ValueError, TypeError):
|
| 285 |
+
continue
|
| 286 |
+
|
| 287 |
+
# Calculate averages
|
| 288 |
+
stats = {}
|
| 289 |
+
for price_type, values in prices.items():
|
| 290 |
+
if values:
|
| 291 |
+
stats[price_type] = {
|
| 292 |
+
'average': sum(values) / len(values),
|
| 293 |
+
'min': min(values),
|
| 294 |
+
'max': max(values),
|
| 295 |
+
'count': len(values)
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
return {
|
| 299 |
+
'status': 'success',
|
| 300 |
+
'mode': 'market_prices',
|
| 301 |
+
'total_records': response.get('total', len(records)),
|
| 302 |
+
'fetched_records': len(records),
|
| 303 |
+
'statistics': stats,
|
| 304 |
+
'records': records[:10], # Return first 10 records for preview
|
| 305 |
+
'filters_applied': {
|
| 306 |
+
'commodity': commodity,
|
| 307 |
+
'state': state,
|
| 308 |
+
'district': district,
|
| 309 |
+
'market': market
|
| 310 |
+
}
|
| 311 |
+
}
|
| 312 |
+
else:
|
| 313 |
+
return {
|
| 314 |
+
'status': 'success',
|
| 315 |
+
'mode': 'market_prices',
|
| 316 |
+
'message': 'No records found for the given filters',
|
| 317 |
+
'filters_applied': {
|
| 318 |
+
'commodity': commodity,
|
| 319 |
+
'state': state,
|
| 320 |
+
'district': district,
|
| 321 |
+
'market': market
|
| 322 |
+
}
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
except Exception as e:
|
| 326 |
+
return {
|
| 327 |
+
'status': 'error',
|
| 328 |
+
'mode': 'market_prices',
|
| 329 |
+
'error': str(e)
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
def get_commodity_list(
|
| 333 |
+
state: Optional[str] = None,
|
| 334 |
+
limit: int = 100
|
| 335 |
+
) -> Dict[str, Any]:
|
| 336 |
+
"""
|
| 337 |
+
Get list of available commodities in the market data.
|
| 338 |
+
|
| 339 |
+
Args:
|
| 340 |
+
state: Optional state filter to get commodities specific to a state
|
| 341 |
+
limit: Number of records to analyze (default: 100)
|
| 342 |
+
|
| 343 |
+
Returns:
|
| 344 |
+
Dictionary with list of unique commodities and their varieties
|
| 345 |
+
"""
|
| 346 |
+
try:
|
| 347 |
+
params = {
|
| 348 |
+
'api-key': _easyfarms_agent.enam_api_key,
|
| 349 |
+
'format': 'json',
|
| 350 |
+
'limit': limit,
|
| 351 |
+
'offset': 0
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
if state:
|
| 355 |
+
params['filters'] = f"state:{state}"
|
| 356 |
+
|
| 357 |
+
response = _easyfarms_agent._fetch_enam_data(params)
|
| 358 |
+
records = response.get('records', [])
|
| 359 |
+
|
| 360 |
+
# Extract unique commodities and varieties
|
| 361 |
+
commodities = {}
|
| 362 |
+
for record in records:
|
| 363 |
+
commodity = record.get('commodity', '').strip()
|
| 364 |
+
variety = record.get('variety', '').strip()
|
| 365 |
+
|
| 366 |
+
if commodity:
|
| 367 |
+
if commodity not in commodities:
|
| 368 |
+
commodities[commodity] = set()
|
| 369 |
+
if variety:
|
| 370 |
+
commodities[commodity].add(variety)
|
| 371 |
+
|
| 372 |
+
# Convert sets to sorted lists
|
| 373 |
+
commodities = {k: sorted(list(v)) for k, v in commodities.items()}
|
| 374 |
+
|
| 375 |
+
return {
|
| 376 |
+
'status': 'success',
|
| 377 |
+
'mode': 'commodity_list',
|
| 378 |
+
'total_commodities': len(commodities),
|
| 379 |
+
'commodities': commodities,
|
| 380 |
+
'state_filter': state
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
except Exception as e:
|
| 384 |
+
return {
|
| 385 |
+
'status': 'error',
|
| 386 |
+
'mode': 'commodity_list',
|
| 387 |
+
'error': str(e)
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
def get_market_locations() -> Dict[str, Any]:
|
| 391 |
+
"""
|
| 392 |
+
Get hierarchical list of market locations (states -> districts -> markets).
|
| 393 |
+
|
| 394 |
+
Returns:
|
| 395 |
+
Dictionary with nested structure of market locations
|
| 396 |
+
"""
|
| 397 |
+
try:
|
| 398 |
+
params = {
|
| 399 |
+
'api-key': _easyfarms_agent.enam_api_key,
|
| 400 |
+
'format': 'json',
|
| 401 |
+
'limit': 100,
|
| 402 |
+
'offset': 0
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
response = _easyfarms_agent._fetch_enam_data(params)
|
| 406 |
+
records = response.get('records', [])
|
| 407 |
+
|
| 408 |
+
# Build hierarchical structure
|
| 409 |
+
locations = {}
|
| 410 |
+
|
| 411 |
+
for record in records:
|
| 412 |
+
state = record.get('state', '').strip()
|
| 413 |
+
district = record.get('district', '').strip()
|
| 414 |
+
market = record.get('market', '').strip()
|
| 415 |
+
|
| 416 |
+
if state:
|
| 417 |
+
if state not in locations:
|
| 418 |
+
locations[state] = {}
|
| 419 |
+
if district:
|
| 420 |
+
if district not in locations[state]:
|
| 421 |
+
locations[state][district] = set()
|
| 422 |
+
if market:
|
| 423 |
+
locations[state][district].add(market)
|
| 424 |
+
|
| 425 |
+
# Convert sets to sorted lists
|
| 426 |
+
for state in locations:
|
| 427 |
+
for district in locations[state]:
|
| 428 |
+
locations[state][district] = sorted(list(locations[state][district]))
|
| 429 |
+
|
| 430 |
+
# Calculate totals
|
| 431 |
+
total_states = len(locations)
|
| 432 |
+
total_districts = sum(len(districts) for districts in locations.values())
|
| 433 |
+
total_markets = sum(
|
| 434 |
+
len(markets)
|
| 435 |
+
for state_data in locations.values()
|
| 436 |
+
for markets in state_data.values()
|
| 437 |
+
)
|
| 438 |
+
|
| 439 |
+
return {
|
| 440 |
+
'status': 'success',
|
| 441 |
+
'mode': 'market_locations',
|
| 442 |
+
'total_states': total_states,
|
| 443 |
+
'total_districts': total_districts,
|
| 444 |
+
'total_markets': total_markets,
|
| 445 |
+
'locations': locations
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
except Exception as e:
|
| 449 |
+
return {
|
| 450 |
+
'status': 'error',
|
| 451 |
+
'mode': 'market_locations',
|
| 452 |
+
'error': str(e)
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
def compare_commodity_prices(
|
| 456 |
+
commodity: str,
|
| 457 |
+
states: Optional[List[str]] = None,
|
| 458 |
+
limit: int = 50
|
| 459 |
+
) -> Dict[str, Any]:
|
| 460 |
+
"""
|
| 461 |
+
Compare prices of a commodity across different states or markets.
|
| 462 |
+
|
| 463 |
+
Args:
|
| 464 |
+
commodity: Name of the commodity to compare
|
| 465 |
+
states: List of states to compare (optional, if not provided compares all)
|
| 466 |
+
limit: Number of records per state
|
| 467 |
+
|
| 468 |
+
Returns:
|
| 469 |
+
Dictionary with price comparison data
|
| 470 |
+
"""
|
| 471 |
+
try:
|
| 472 |
+
comparison_data = {}
|
| 473 |
+
|
| 474 |
+
if states:
|
| 475 |
+
# Compare specific states
|
| 476 |
+
for state in states:
|
| 477 |
+
params = {
|
| 478 |
+
'api-key': _easyfarms_agent.enam_api_key,
|
| 479 |
+
'format': 'json',
|
| 480 |
+
'limit': limit,
|
| 481 |
+
'offset': 0,
|
| 482 |
+
'filters': f"commodity:{commodity},state:{state}"
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
response = _easyfarms_agent._fetch_enam_data(params)
|
| 486 |
+
records = response.get('records', [])
|
| 487 |
+
|
| 488 |
+
if records:
|
| 489 |
+
prices = []
|
| 490 |
+
for r in records:
|
| 491 |
+
modal_price = r.get('modal_price')
|
| 492 |
+
if modal_price:
|
| 493 |
+
try:
|
| 494 |
+
price = float(modal_price)
|
| 495 |
+
if price > 0:
|
| 496 |
+
prices.append(price)
|
| 497 |
+
except (ValueError, TypeError):
|
| 498 |
+
continue
|
| 499 |
+
|
| 500 |
+
if prices:
|
| 501 |
+
comparison_data[state] = {
|
| 502 |
+
'average_price': sum(prices) / len(prices),
|
| 503 |
+
'min_price': min(prices),
|
| 504 |
+
'max_price': max(prices),
|
| 505 |
+
'sample_count': len(prices),
|
| 506 |
+
'sample_markets': list(set(r.get('market', '') for r in records[:5] if r.get('market')))
|
| 507 |
+
}
|
| 508 |
+
else:
|
| 509 |
+
# Get data for all states
|
| 510 |
+
params = {
|
| 511 |
+
'api-key': _easyfarms_agent.enam_api_key,
|
| 512 |
+
'format': 'json',
|
| 513 |
+
'limit': 100,
|
| 514 |
+
'offset': 0,
|
| 515 |
+
'filters': f"commodity:{commodity}"
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
response = _easyfarms_agent._fetch_enam_data(params)
|
| 519 |
+
records = response.get('records', [])
|
| 520 |
+
|
| 521 |
+
# Group by state
|
| 522 |
+
state_data = {}
|
| 523 |
+
for record in records:
|
| 524 |
+
state = record.get('state', '')
|
| 525 |
+
if state:
|
| 526 |
+
if state not in state_data:
|
| 527 |
+
state_data[state] = []
|
| 528 |
+
modal_price = record.get('modal_price')
|
| 529 |
+
if modal_price:
|
| 530 |
+
try:
|
| 531 |
+
price = float(modal_price)
|
| 532 |
+
if price > 0:
|
| 533 |
+
state_data[state].append(price)
|
| 534 |
+
except (ValueError, TypeError):
|
| 535 |
+
continue
|
| 536 |
+
|
| 537 |
+
# Calculate statistics per state
|
| 538 |
+
for state, prices in state_data.items():
|
| 539 |
+
if prices:
|
| 540 |
+
comparison_data[state] = {
|
| 541 |
+
'average_price': sum(prices) / len(prices),
|
| 542 |
+
'min_price': min(prices),
|
| 543 |
+
'max_price': max(prices),
|
| 544 |
+
'sample_count': len(prices)
|
| 545 |
+
}
|
| 546 |
+
|
| 547 |
+
# Find best and worst prices
|
| 548 |
+
if comparison_data:
|
| 549 |
+
avg_prices = {state: data['average_price'] for state, data in comparison_data.items()}
|
| 550 |
+
best_state = min(avg_prices, key=avg_prices.get)
|
| 551 |
+
worst_state = max(avg_prices, key=avg_prices.get)
|
| 552 |
+
|
| 553 |
+
return {
|
| 554 |
+
'status': 'success',
|
| 555 |
+
'mode': 'price_comparison',
|
| 556 |
+
'commodity': commodity,
|
| 557 |
+
'comparison_data': comparison_data,
|
| 558 |
+
'summary': {
|
| 559 |
+
'best_price_state': best_state,
|
| 560 |
+
'best_average_price': avg_prices[best_state],
|
| 561 |
+
'worst_price_state': worst_state,
|
| 562 |
+
'worst_average_price': avg_prices[worst_state],
|
| 563 |
+
'price_range': avg_prices[worst_state] - avg_prices[best_state],
|
| 564 |
+
'states_compared': len(comparison_data)
|
| 565 |
+
}
|
| 566 |
+
}
|
| 567 |
+
else:
|
| 568 |
+
return {
|
| 569 |
+
'status': 'success',
|
| 570 |
+
'mode': 'price_comparison',
|
| 571 |
+
'commodity': commodity,
|
| 572 |
+
'message': 'No price data found for the specified commodity'
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
except Exception as e:
|
| 576 |
+
return {
|
| 577 |
+
'status': 'error',
|
| 578 |
+
'mode': 'price_comparison',
|
| 579 |
+
'error': str(e)
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
def get_crop_recommendation(
|
| 583 |
+
N: int,
|
| 584 |
+
P: int,
|
| 585 |
+
K: int,
|
| 586 |
+
temperature: float,
|
| 587 |
+
humidity: float,
|
| 588 |
+
ph: float = None,
|
| 589 |
+
rainfall: float = 100
|
| 590 |
+
) -> Dict[str, Any]:
|
| 591 |
+
try:
|
| 592 |
+
data = {
|
| 593 |
+
"N": N, "P": P, "K": K,
|
| 594 |
+
"temperature": temperature,
|
| 595 |
+
"humidity": humidity,
|
| 596 |
+
"ph": ph if ph is not None else 6.5,
|
| 597 |
+
"rainfall": rainfall
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
response = _easyfarms_agent._make_request_with_retry(
|
| 601 |
+
"POST",
|
| 602 |
+
_easyfarms_agent.endpoints["crop"],
|
| 603 |
+
data=data
|
| 604 |
+
)
|
| 605 |
+
|
| 606 |
+
if isinstance(response, dict):
|
| 607 |
+
response['input_parameters'] = data
|
| 608 |
+
if 'mode' not in response:
|
| 609 |
+
response['mode'] = 'crop_recommendation'
|
| 610 |
+
|
| 611 |
+
logger.info(f"Crop recommendation response: {response}")
|
| 612 |
+
return response
|
| 613 |
+
|
| 614 |
+
except Exception as e:
|
| 615 |
+
error_result = {
|
| 616 |
+
"error": str(e),
|
| 617 |
+
"status": "error",
|
| 618 |
+
"mode": "crop_recommendation"
|
| 619 |
+
}
|
| 620 |
+
logger.error(f"Crop recommendation failed: {e}")
|
| 621 |
+
return error_result
|
| 622 |
+
|
| 623 |
+
def get_fertilizer_recommendation(
|
| 624 |
+
crop: str,
|
| 625 |
+
soil: str,
|
| 626 |
+
temperature: float,
|
| 627 |
+
humidity: float,
|
| 628 |
+
moisture: float,
|
| 629 |
+
N: int,
|
| 630 |
+
P: int,
|
| 631 |
+
K: int
|
| 632 |
+
) -> Dict[str, Any]:
|
| 633 |
+
try:
|
| 634 |
+
soil_code = _easyfarms_agent.soil_mapping.get(soil.lower(), soil)
|
| 635 |
+
crop_code = _easyfarms_agent.fertilizer_crop_mapping.get(crop.lower(), crop)
|
| 636 |
+
|
| 637 |
+
data = {
|
| 638 |
+
"temperature": temperature,
|
| 639 |
+
"humidity": humidity,
|
| 640 |
+
"moisture": moisture,
|
| 641 |
+
"N": N, "P": P, "K": K,
|
| 642 |
+
"soil": soil_code,
|
| 643 |
+
"crop": crop_code
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
response = _easyfarms_agent._make_request_with_retry(
|
| 647 |
+
"POST",
|
| 648 |
+
_easyfarms_agent.endpoints["fertilizer"],
|
| 649 |
+
data=data
|
| 650 |
+
)
|
| 651 |
+
|
| 652 |
+
if isinstance(response, dict):
|
| 653 |
+
response['input_parameters'] = {
|
| 654 |
+
**data,
|
| 655 |
+
"original_soil": soil,
|
| 656 |
+
"original_crop": crop
|
| 657 |
+
}
|
| 658 |
+
if 'mode' not in response:
|
| 659 |
+
response['mode'] = 'fertilizer_recommendation'
|
| 660 |
+
|
| 661 |
+
logger.info(f"Fertilizer recommendation response: {response}")
|
| 662 |
+
return response
|
| 663 |
+
|
| 664 |
+
except Exception as e:
|
| 665 |
+
error_result = {
|
| 666 |
+
"status": "error",
|
| 667 |
+
"error": str(e),
|
| 668 |
+
"mode": "fertilizer_recommendation"
|
| 669 |
+
}
|
| 670 |
+
logger.error(f"Fertilizer recommendation failed: {e}")
|
| 671 |
+
return error_result
|
| 672 |
+
|
| 673 |
+
def detect_plant_disease(
|
| 674 |
+
crop: str,
|
| 675 |
+
image_path: str, # Can now be a URL or local path
|
| 676 |
+
language: str = "en"
|
| 677 |
+
) -> Dict[str, Any]:
|
| 678 |
+
"""
|
| 679 |
+
Detect plant diseases from an image URL or local path.
|
| 680 |
+
"""
|
| 681 |
+
temp_image_path = None
|
| 682 |
+
try:
|
| 683 |
+
# Check if the image_path is a URL
|
| 684 |
+
if image_path.startswith(('http://', 'https://')):
|
| 685 |
+
logger.info(f"Downloading image from URL: {image_path}")
|
| 686 |
+
response = requests.get(image_path, stream=True, timeout=30)
|
| 687 |
+
response.raise_for_status()
|
| 688 |
+
|
| 689 |
+
# Create a temporary file to store the downloaded image
|
| 690 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".jpg") as temp_file:
|
| 691 |
+
for chunk in response.iter_content(chunk_size=8192):
|
| 692 |
+
temp_file.write(chunk)
|
| 693 |
+
temp_image_path = temp_file.name
|
| 694 |
+
|
| 695 |
+
# The actual path to use is now the temporary file's path
|
| 696 |
+
path_to_process = temp_image_path
|
| 697 |
+
else:
|
| 698 |
+
# If not a URL, assume it's a local file path
|
| 699 |
+
path_to_process = image_path
|
| 700 |
+
|
| 701 |
+
# Check if the final file path exists
|
| 702 |
+
if not os.path.exists(path_to_process):
|
| 703 |
+
return {
|
| 704 |
+
"error": f"Image file not found at path: {path_to_process}",
|
| 705 |
+
"status": "error", "mode": "disease_detection"
|
| 706 |
+
}
|
| 707 |
+
|
| 708 |
+
crop_key = _easyfarms_agent.disease_crop_mapping.get(crop.lower(), crop.lower())
|
| 709 |
+
|
| 710 |
+
headers = { 'accept': '*/*' }
|
| 711 |
+
|
| 712 |
+
with open(path_to_process, "rb") as f:
|
| 713 |
+
files = { 'image': ('blob', f, 'image/jpeg') }
|
| 714 |
+
data = { 'crop': crop_key, 'language': language }
|
| 715 |
+
|
| 716 |
+
response = _easyfarms_agent.session.post(
|
| 717 |
+
_easyfarms_agent.endpoints["disease"],
|
| 718 |
+
files=files, data=data, headers=headers,
|
| 719 |
+
timeout=_easyfarms_agent.timeout
|
| 720 |
+
)
|
| 721 |
+
response.raise_for_status()
|
| 722 |
+
|
| 723 |
+
result = response.json()
|
| 724 |
+
result.update({ "status": "success", "mode": "disease_detection" })
|
| 725 |
+
return result
|
| 726 |
+
|
| 727 |
+
except Exception as e:
|
| 728 |
+
logger.error(f"Disease detection failed: {e}")
|
| 729 |
+
return { "error": str(e), "status": "error", "mode": "disease_detection" }
|
| 730 |
+
finally:
|
| 731 |
+
# Clean up the temporary file if it was created
|
| 732 |
+
if temp_image_path and os.path.exists(temp_image_path):
|
| 733 |
+
try:
|
| 734 |
+
os.remove(temp_image_path)
|
| 735 |
+
logger.info(f"Removed temporary image file: {temp_image_path}")
|
| 736 |
+
except OSError:
|
| 737 |
+
logger.warning(f"Failed to remove temporary file: {temp_image_path}")
|
| 738 |
+
|
| 739 |
+
def get_supported_options(mode: str) -> Dict[str, Any]:
|
| 740 |
+
try:
|
| 741 |
+
result = {"status": "success", "mode": "supported_options"}
|
| 742 |
+
|
| 743 |
+
if mode == "fertilizer_crops":
|
| 744 |
+
result["fertilizer_crops"] = list(_easyfarms_agent.fertilizer_crop_mapping.keys())
|
| 745 |
+
elif mode == "disease_crops":
|
| 746 |
+
result["disease_crops"] = list(_easyfarms_agent.disease_crop_mapping.keys())
|
| 747 |
+
elif mode == "soil_types":
|
| 748 |
+
result["soil_types"] = list(_easyfarms_agent.soil_mapping.keys())
|
| 749 |
+
elif mode == "all":
|
| 750 |
+
result.update({
|
| 751 |
+
"fertilizer_crops": list(_easyfarms_agent.fertilizer_crop_mapping.keys()),
|
| 752 |
+
"disease_crops": list(_easyfarms_agent.disease_crop_mapping.keys()),
|
| 753 |
+
"soil_types": list(_easyfarms_agent.soil_mapping.keys())
|
| 754 |
+
})
|
| 755 |
+
else:
|
| 756 |
+
return {
|
| 757 |
+
"error": f"Invalid mode: {mode}. Use 'fertilizer_crops', 'disease_crops', 'soil_types', or 'all'",
|
| 758 |
+
"status": "error"
|
| 759 |
+
}
|
| 760 |
+
|
| 761 |
+
return result
|
| 762 |
+
|
| 763 |
+
except Exception as e:
|
| 764 |
+
return {
|
| 765 |
+
"error": str(e),
|
| 766 |
+
"status": "error",
|
| 767 |
+
"mode": "supported_options"
|
| 768 |
+
}
|
| 769 |
|
| 770 |
# =============================================================================
|
| 771 |
+
# ENHANCED FUNCTION SCHEMAS WITH MARKET DATA
|
| 772 |
# =============================================================================
|
| 773 |
|
|
|
|
| 774 |
EASYFARMS_FUNCTION_SCHEMAS = [
|
| 775 |
{
|
| 776 |
"name": "get_crop_recommendation",
|
|
|
|
| 894 |
},
|
| 895 |
"image_path": {
|
| 896 |
"type": "string",
|
| 897 |
+
"description": "Path to the plant/leaf image file or image URL"
|
| 898 |
},
|
| 899 |
"language": {
|
| 900 |
"type": "string",
|
|
|
|
| 906 |
"required": ["crop", "image_path"]
|
| 907 |
}
|
| 908 |
},
|
| 909 |
+
{
|
| 910 |
+
"name": "get_market_prices",
|
| 911 |
+
"description": "Get current market prices for agricultural commodities from Indian mandis (markets). Returns price data with statistics.",
|
| 912 |
+
"parameters": {
|
| 913 |
+
"type": "object",
|
| 914 |
+
"properties": {
|
| 915 |
+
"commodity": {
|
| 916 |
+
"type": "string",
|
| 917 |
+
"description": "Name of the commodity (e.g., 'Tomato', 'Wheat', 'Rice')"
|
| 918 |
+
},
|
| 919 |
+
"state": {
|
| 920 |
+
"type": "string",
|
| 921 |
+
"description": "State name (e.g., 'Gujarat', 'Maharashtra')"
|
| 922 |
+
},
|
| 923 |
+
"district": {
|
| 924 |
+
"type": "string",
|
| 925 |
+
"description": "District name"
|
| 926 |
+
},
|
| 927 |
+
"market": {
|
| 928 |
+
"type": "string",
|
| 929 |
+
"description": "Market/mandi name"
|
| 930 |
+
},
|
| 931 |
+
"limit": {
|
| 932 |
+
"type": "integer",
|
| 933 |
+
"description": "Number of records to fetch (default: 25, max: 100)",
|
| 934 |
+
"minimum": 1,
|
| 935 |
+
"maximum": 100,
|
| 936 |
+
"default": 25
|
| 937 |
+
}
|
| 938 |
+
}
|
| 939 |
+
}
|
| 940 |
+
},
|
| 941 |
+
{
|
| 942 |
+
"name": "get_commodity_list",
|
| 943 |
+
"description": "Get list of available agricultural commodities with their varieties in the market data.",
|
| 944 |
+
"parameters": {
|
| 945 |
+
"type": "object",
|
| 946 |
+
"properties": {
|
| 947 |
+
"state": {
|
| 948 |
+
"type": "string",
|
| 949 |
+
"description": "Optional state filter to get commodities specific to a state"
|
| 950 |
+
},
|
| 951 |
+
"limit": {
|
| 952 |
+
"type": "integer",
|
| 953 |
+
"description": "Number of records to analyze (default: 100)",
|
| 954 |
+
"minimum": 1,
|
| 955 |
+
"maximum": 500,
|
| 956 |
+
"default": 100
|
| 957 |
+
}
|
| 958 |
+
}
|
| 959 |
+
}
|
| 960 |
+
},
|
| 961 |
+
{
|
| 962 |
+
"name": "get_market_locations",
|
| 963 |
+
"description": "Get hierarchical list of market locations organized by states, districts, and markets.",
|
| 964 |
+
"parameters": {
|
| 965 |
+
"type": "object",
|
| 966 |
+
"properties": {}
|
| 967 |
+
}
|
| 968 |
+
},
|
| 969 |
+
{
|
| 970 |
+
"name": "compare_commodity_prices",
|
| 971 |
+
"description": "Compare prices of a commodity across different states or markets to find the best deals.",
|
| 972 |
+
"parameters": {
|
| 973 |
+
"type": "object",
|
| 974 |
+
"properties": {
|
| 975 |
+
"commodity": {
|
| 976 |
+
"type": "string",
|
| 977 |
+
"description": "Name of the commodity to compare"
|
| 978 |
+
},
|
| 979 |
+
"states": {
|
| 980 |
+
"type": "array",
|
| 981 |
+
"items": {
|
| 982 |
+
"type": "string"
|
| 983 |
+
},
|
| 984 |
+
"description": "List of states to compare (optional, if not provided compares all)"
|
| 985 |
+
},
|
| 986 |
+
"limit": {
|
| 987 |
+
"type": "integer",
|
| 988 |
+
"description": "Number of records per state (default: 50)",
|
| 989 |
+
"minimum": 1,
|
| 990 |
+
"maximum": 100,
|
| 991 |
+
"default": 50
|
| 992 |
+
}
|
| 993 |
+
},
|
| 994 |
+
"required": ["commodity"]
|
| 995 |
+
}
|
| 996 |
+
},
|
| 997 |
{
|
| 998 |
"name": "get_supported_options",
|
| 999 |
"description": "Get lists of supported crops and soil types for different analysis modes. Useful for showing available options to users.",
|
|
|
|
| 1011 |
}
|
| 1012 |
]
|
| 1013 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1014 |
# =============================================================================
|
| 1015 |
# FUNCTION MAPPING FOR AI AGENTS
|
| 1016 |
# =============================================================================
|
| 1017 |
|
|
|
|
| 1018 |
EASYFARMS_FUNCTIONS = {
|
| 1019 |
"get_crop_recommendation": get_crop_recommendation,
|
| 1020 |
"get_fertilizer_recommendation": get_fertilizer_recommendation,
|
| 1021 |
"detect_plant_disease": detect_plant_disease,
|
| 1022 |
+
"get_supported_options": get_supported_options,
|
| 1023 |
+
"get_market_prices": get_market_prices,
|
| 1024 |
+
"get_commodity_list": get_commodity_list,
|
| 1025 |
+
"get_market_locations": get_market_locations,
|
| 1026 |
+
"compare_commodity_prices": compare_commodity_prices
|
| 1027 |
}
|
| 1028 |
|
| 1029 |
# =============================================================================
|
|
|
|
| 1073 |
# =============================================================================
|
| 1074 |
|
| 1075 |
if __name__ == "__main__":
|
| 1076 |
+
print("=== Enhanced EasyFarms Agent with Market Data ===\n")
|
| 1077 |
+
|
| 1078 |
+
print("Available Functions:")
|
| 1079 |
+
for name in get_function_names():
|
| 1080 |
+
print(f" - {name}")
|
| 1081 |
+
print()
|
| 1082 |
+
|
| 1083 |
+
# Example 1: Get market prices
|
| 1084 |
+
print("1. Getting tomato prices in Gujarat:")
|
| 1085 |
+
market_result = execute_easyfarms_function(
|
| 1086 |
+
"get_market_prices",
|
| 1087 |
+
commodity="Tomato",
|
| 1088 |
+
state="Gujarat",
|
| 1089 |
+
limit=10
|
| 1090 |
+
)
|
| 1091 |
+
print(json.dumps(market_result, indent=2))
|
| 1092 |
+
print()
|
| 1093 |
|
| 1094 |
+
# Example 2: Compare commodity prices
|
| 1095 |
+
print("2. Comparing wheat prices across states:")
|
| 1096 |
+
comparison_result = execute_easyfarms_function(
|
| 1097 |
+
"compare_commodity_prices",
|
| 1098 |
+
commodity="Wheat",
|
| 1099 |
+
states=["Punjab", "Haryana", "Uttar Pradesh"]
|
| 1100 |
+
)
|
| 1101 |
+
print(json.dumps(comparison_result, indent=2))
|
| 1102 |
+
print()
|
| 1103 |
|
| 1104 |
+
# Example 3: Get commodity list
|
| 1105 |
+
print("3. Getting available commodities:")
|
| 1106 |
+
commodity_result = execute_easyfarms_function(
|
| 1107 |
+
"get_commodity_list",
|
| 1108 |
+
limit=20
|
| 1109 |
+
)
|
| 1110 |
+
print(f"Total commodities: {commodity_result.get('total_commodities', 0)}")
|
| 1111 |
+
print()
|
| 1112 |
|
| 1113 |
+
# Example 4: Crop recommendation (existing function)
|
| 1114 |
+
print("4. Getting crop recommendation:")
|
| 1115 |
crop_result = execute_easyfarms_function(
|
| 1116 |
"get_crop_recommendation",
|
| 1117 |
N=90, P=42, K=43,
|
|
|
|
| 1120 |
ph=6.5,
|
| 1121 |
rainfall=202.9
|
| 1122 |
)
|
| 1123 |
+
print(json.dumps(crop_result, indent=2))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
functions_calling.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
WEATHER_FUNCTIONS = [
|
| 2 |
+
{
|
| 3 |
+
"type": "function",
|
| 4 |
+
"function": {
|
| 5 |
+
"name": "get_weather_alerts",
|
| 6 |
+
"description": "Get weather alerts for one or more locations in India. Can search by city, state, district, or region name.",
|
| 7 |
+
"parameters": {
|
| 8 |
+
"type": "object",
|
| 9 |
+
"properties": {
|
| 10 |
+
"locations": {
|
| 11 |
+
"type": "array",
|
| 12 |
+
"items": {
|
| 13 |
+
"type": "string"
|
| 14 |
+
},
|
| 15 |
+
"description": "List of locations to search for (up to 5). Can be city names, state names, districts, etc.",
|
| 16 |
+
"maxItems": 5,
|
| 17 |
+
"minItems": 1
|
| 18 |
+
},
|
| 19 |
+
"include_details": {
|
| 20 |
+
"type": "boolean",
|
| 21 |
+
"description": "Whether to include detailed alert information like warning messages and coordinates",
|
| 22 |
+
"default": True
|
| 23 |
+
}
|
| 24 |
+
},
|
| 25 |
+
"required": ["locations"]
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"type": "function",
|
| 31 |
+
"function": {
|
| 32 |
+
"name": "get_alert_summary",
|
| 33 |
+
"description": "Get a summary of all current weather alerts by severity level",
|
| 34 |
+
"parameters": {
|
| 35 |
+
"type": "object",
|
| 36 |
+
"properties": {},
|
| 37 |
+
"required": []
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
},
|
| 41 |
+
{
|
| 42 |
+
"type": "function",
|
| 43 |
+
"function": {
|
| 44 |
+
"name": "get_available_locations",
|
| 45 |
+
"description": "Get a list of all locations that currently have weather alerts",
|
| 46 |
+
"parameters": {
|
| 47 |
+
"type": "object",
|
| 48 |
+
"properties": {
|
| 49 |
+
"limit": {
|
| 50 |
+
"type": "integer",
|
| 51 |
+
"description": "Maximum number of locations to return",
|
| 52 |
+
"default": 50,
|
| 53 |
+
"minimum": 1,
|
| 54 |
+
"maximum": 200
|
| 55 |
+
}
|
| 56 |
+
},
|
| 57 |
+
"required": []
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
]
|
server.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# flask_app.py
|
| 2 |
+
|
| 3 |
+
from flask import Flask, request, jsonify, render_template
|
| 4 |
+
from app import EasyFarmsAssistant
|
| 5 |
+
from conversation_manager import ConversationManager # Make sure this import is present
|
| 6 |
+
import logging
|
| 7 |
+
import uuid
|
| 8 |
+
import os
|
| 9 |
+
|
| 10 |
+
# Configure logging
|
| 11 |
+
logging.basicConfig(level=logging.INFO)
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
# Initialize the Flask application
|
| 15 |
+
app = Flask(__name__)
|
| 16 |
+
|
| 17 |
+
# --- Initialize Core Services ---
|
| 18 |
+
# This setup assumes your .env file is correctly configured with Supabase credentials.
|
| 19 |
+
try:
|
| 20 |
+
conv_manager = ConversationManager()
|
| 21 |
+
assistant = EasyFarmsAssistant(manager=conv_manager)
|
| 22 |
+
logger.info("EasyFarmsAssistant and ConversationManager initialized successfully.")
|
| 23 |
+
except Exception as e:
|
| 24 |
+
logger.error(f"FATAL: Could not initialize services. Error: {e}")
|
| 25 |
+
assistant = None
|
| 26 |
+
conv_manager = None
|
| 27 |
+
|
| 28 |
+
# --- Frontend Serving Route ---
|
| 29 |
+
@app.route('/')
|
| 30 |
+
def index():
|
| 31 |
+
"""Serves the main chat application page (index.html)."""
|
| 32 |
+
return render_template('index.html')
|
| 33 |
+
|
| 34 |
+
# --- API Endpoints ---
|
| 35 |
+
|
| 36 |
+
@app.route('/config', methods=['GET'])
|
| 37 |
+
def get_config():
|
| 38 |
+
"""Provides public configuration keys to the frontend."""
|
| 39 |
+
return jsonify({
|
| 40 |
+
'imgbb_api_key': os.getenv('IMGBB_API_KEY')
|
| 41 |
+
})
|
| 42 |
+
|
| 43 |
+
@app.route('/chat', methods=['POST'])
|
| 44 |
+
def chat():
|
| 45 |
+
"""Handles incoming user messages and returns the assistant's response."""
|
| 46 |
+
if not assistant:
|
| 47 |
+
return jsonify({"error": "Assistant is not available due to an initialization error."}), 503
|
| 48 |
+
|
| 49 |
+
# The request is multipart/form-data to handle potential image uploads
|
| 50 |
+
data = request.form
|
| 51 |
+
user_message = data.get('message')
|
| 52 |
+
session_id = data.get('session_id')
|
| 53 |
+
image_url = data.get('image_url') # The permanent URL from ImgBB
|
| 54 |
+
|
| 55 |
+
# A message must contain either text or an image
|
| 56 |
+
if not user_message and not image_url:
|
| 57 |
+
return jsonify({"error": "Cannot process an empty message."}), 400
|
| 58 |
+
|
| 59 |
+
# If no session_id is provided by the client, it's a new conversation
|
| 60 |
+
if not session_id or session_id == 'null' or session_id == 'undefined':
|
| 61 |
+
session_id = str(uuid.uuid4())
|
| 62 |
+
logger.info(f"No session_id provided. Creating new session: {session_id}")
|
| 63 |
+
|
| 64 |
+
# Call the assistant's core logic, now passing the image_url separately
|
| 65 |
+
response_content = assistant.process_query(
|
| 66 |
+
user_message=user_message or "", # Ensure user_message is a string
|
| 67 |
+
session_id=session_id,
|
| 68 |
+
image_url=image_url
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
return jsonify({
|
| 72 |
+
"response": response_content,
|
| 73 |
+
"session_id": session_id
|
| 74 |
+
})
|
| 75 |
+
|
| 76 |
+
@app.route('/history/sessions', methods=['GET'])
|
| 77 |
+
def get_sessions():
|
| 78 |
+
"""Fetches a list of all conversation sessions for the sidebar."""
|
| 79 |
+
if not conv_manager:
|
| 80 |
+
return jsonify({"error": "Conversation manager not available"}), 503
|
| 81 |
+
try:
|
| 82 |
+
# Fetch session_id and the full history to generate a title
|
| 83 |
+
all_conversations = conv_manager.supabase.table('conversations').select('session_id, history').execute()
|
| 84 |
+
|
| 85 |
+
sessions = []
|
| 86 |
+
for conv in all_conversations.data:
|
| 87 |
+
title = "New Chat" # Default title
|
| 88 |
+
# Generate a title from the first user message in the history
|
| 89 |
+
if conv.get('history') and len(conv['history']) > 0:
|
| 90 |
+
first_message_content = conv['history'][0].get('content')
|
| 91 |
+
if first_message_content:
|
| 92 |
+
# Truncate for display
|
| 93 |
+
title = first_message_content[:35] + '...' if len(first_message_content) > 35 else first_message_content
|
| 94 |
+
else: # If the first message was only an image
|
| 95 |
+
title = "Image Query"
|
| 96 |
+
|
| 97 |
+
sessions.append({
|
| 98 |
+
"session_id": conv.get('session_id'),
|
| 99 |
+
"title": title
|
| 100 |
+
})
|
| 101 |
+
|
| 102 |
+
return jsonify(sessions)
|
| 103 |
+
except Exception as e:
|
| 104 |
+
logger.error(f"Error fetching sessions from Supabase: {e}")
|
| 105 |
+
return jsonify({"error": "Could not fetch conversation sessions"}), 500
|
| 106 |
+
|
| 107 |
+
@app.route('/history/messages/<session_id>', methods=['GET'])
|
| 108 |
+
def get_messages(session_id):
|
| 109 |
+
"""Fetches the full, structured message history for a given session_id."""
|
| 110 |
+
if not conv_manager:
|
| 111 |
+
return jsonify({"error": "Conversation manager not available"}), 503
|
| 112 |
+
try:
|
| 113 |
+
history = conv_manager.get_history(session_id)
|
| 114 |
+
return jsonify(history)
|
| 115 |
+
except Exception as e:
|
| 116 |
+
logger.error(f"Error fetching messages for session {session_id}: {e}")
|
| 117 |
+
return jsonify({"error": "Could not fetch message history"}), 500
|
| 118 |
+
|
| 119 |
+
@app.route('/clear', methods=['POST'])
|
| 120 |
+
def clear_history():
|
| 121 |
+
"""Deletes a conversation history from the database."""
|
| 122 |
+
if not assistant:
|
| 123 |
+
return jsonify({"error": "Assistant is not available."}), 503
|
| 124 |
+
|
| 125 |
+
data = request.get_json()
|
| 126 |
+
session_id = data.get('session_id')
|
| 127 |
+
|
| 128 |
+
if not session_id:
|
| 129 |
+
return jsonify({"error": "Missing 'session_id' in request body"}), 400
|
| 130 |
+
|
| 131 |
+
if assistant.clear_history(session_id):
|
| 132 |
+
return jsonify({"status": "success", "message": f"History for session {session_id} was cleared."})
|
| 133 |
+
else:
|
| 134 |
+
return jsonify({"error": "Failed to clear history."}), 500
|
| 135 |
+
|
| 136 |
+
# --- Main Execution ---
|
| 137 |
+
if __name__ == '__main__':
|
| 138 |
+
# Set debug=False for production
|
| 139 |
+
app.run(debug=True, port=5000)
|
static/script.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 2 |
+
|
| 3 |
+
// --- DOM Elements ---
|
| 4 |
+
const chatArea = document.getElementById('chat-area');
|
| 5 |
+
const closeSidebarBtn = document.getElementById('close-sidebar-btn');
|
| 6 |
+
const messageInput = document.getElementById('message-input');
|
| 7 |
+
const sendBtn = document.getElementById('send-btn');
|
| 8 |
+
const newChatBtn = document.getElementById('new-chat-btn');
|
| 9 |
+
const chatMessages = document.getElementById('chat-messages');
|
| 10 |
+
const chatHistoryList = document.getElementById('chat-history-list');
|
| 11 |
+
const menuToggle = document.getElementById('menu-toggle');
|
| 12 |
+
const appContainer = document.getElementById('app-container');
|
| 13 |
+
const chatTitle = document.getElementById('chat-title');
|
| 14 |
+
const imageUploadBtn = document.getElementById('image-upload-btn');
|
| 15 |
+
const imageUploadInput = document.getElementById('image-upload-input');
|
| 16 |
+
const imagePreviewContainer = document.getElementById('image-preview-container');
|
| 17 |
+
const imagePreview = document.getElementById('image-preview');
|
| 18 |
+
const removeImageBtn = document.getElementById('remove-image-btn');
|
| 19 |
+
|
| 20 |
+
// --- State ---
|
| 21 |
+
let currentSessionId = null;
|
| 22 |
+
let conversationsCache = {};
|
| 23 |
+
let selectedImageFile = null;
|
| 24 |
+
let imgbbApiKey = '';
|
| 25 |
+
|
| 26 |
+
// --- Initialization ---
|
| 27 |
+
const init = async () => {
|
| 28 |
+
if (!messageInput || !sendBtn || !imageUploadBtn || !imagePreviewContainer) {
|
| 29 |
+
console.error("Critical UI elements not found. Check HTML IDs.");
|
| 30 |
+
return;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
await fetchConfig();
|
| 34 |
+
loadCache();
|
| 35 |
+
await renderChatHistoryFromAPI();
|
| 36 |
+
|
| 37 |
+
// Event Listeners
|
| 38 |
+
sendBtn.addEventListener('click', sendMessage);
|
| 39 |
+
messageInput.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } });
|
| 40 |
+
|
| 41 |
+
imageUploadBtn.addEventListener('click', () => imageUploadInput.click());
|
| 42 |
+
imageUploadInput.addEventListener('change', handleImageSelect);
|
| 43 |
+
removeImageBtn.addEventListener('click', removeSelectedImage);
|
| 44 |
+
|
| 45 |
+
// Sidebar Listeners
|
| 46 |
+
newChatBtn.addEventListener('click', () => { startNewChat(); closeSidebar(); });
|
| 47 |
+
menuToggle.addEventListener('click', (event) => {
|
| 48 |
+
event.stopPropagation();
|
| 49 |
+
appContainer.classList.toggle('sidebar-visible');
|
| 50 |
+
});
|
| 51 |
+
closeSidebarBtn.addEventListener('click', (event) => {
|
| 52 |
+
event.stopPropagation();
|
| 53 |
+
closeSidebar();
|
| 54 |
+
});
|
| 55 |
+
chatArea.addEventListener('click', () => {
|
| 56 |
+
if (appContainer.classList.contains('sidebar-visible')) {
|
| 57 |
+
closeSidebar();
|
| 58 |
+
}
|
| 59 |
+
});
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
const fetchConfig = async () => {
|
| 63 |
+
try {
|
| 64 |
+
const response = await fetch('/config');
|
| 65 |
+
const config = await response.json();
|
| 66 |
+
imgbbApiKey = config.imgbb_api_key;
|
| 67 |
+
if (!imgbbApiKey) { console.error("ImgBB API Key is missing."); }
|
| 68 |
+
} catch (error) { console.error('Failed to fetch config:', error); }
|
| 69 |
+
};
|
| 70 |
+
|
| 71 |
+
// --- Core Chat Functions ---
|
| 72 |
+
const sendMessage = async () => {
|
| 73 |
+
const messageText = messageInput.value.trim();
|
| 74 |
+
if (!messageText && !selectedImageFile) return;
|
| 75 |
+
displayMessage({ role: 'user', content: messageText, imageUrl: selectedImageFile });
|
| 76 |
+
const loadingIndicator = displayMessage({ role: 'assistant', content: 'Thinking...', isLoading: true });
|
| 77 |
+
try {
|
| 78 |
+
let permanentImageUrl = null;
|
| 79 |
+
if (selectedImageFile) { permanentImageUrl = await uploadImageToImgBB(selectedImageFile); }
|
| 80 |
+
const formData = new FormData();
|
| 81 |
+
formData.append('message', messageText);
|
| 82 |
+
formData.append('session_id', currentSessionId);
|
| 83 |
+
if (permanentImageUrl) { formData.append('image_url', permanentImageUrl); }
|
| 84 |
+
const response = await fetch('/chat', { method: 'POST', body: formData });
|
| 85 |
+
if (!response.ok) throw new Error('Network response was not ok.');
|
| 86 |
+
const data = await response.json();
|
| 87 |
+
chatMessages.removeChild(loadingIndicator);
|
| 88 |
+
displayMessage({ role: 'assistant', content: data.response });
|
| 89 |
+
updateCache(data.session_id, { content: messageText, imageUrl: permanentImageUrl }, { content: data.response });
|
| 90 |
+
} catch (error) {
|
| 91 |
+
console.error('Error sending message:', error);
|
| 92 |
+
loadingIndicator.innerHTML = marked.parse("Sorry, something went wrong.");
|
| 93 |
+
loadingIndicator.classList.remove('loading');
|
| 94 |
+
} finally {
|
| 95 |
+
messageInput.value = ''; messageInput.style.height = 'auto'; removeSelectedImage();
|
| 96 |
+
}
|
| 97 |
+
};
|
| 98 |
+
|
| 99 |
+
const uploadImageToImgBB = async (imageFile) => {
|
| 100 |
+
if (!imgbbApiKey) throw new Error("ImgBB API Key not configured.");
|
| 101 |
+
const formData = new FormData();
|
| 102 |
+
formData.append('image', imageFile);
|
| 103 |
+
formData.append('key', imgbbApiKey);
|
| 104 |
+
const response = await fetch('https://api.imgbb.com/1/upload', { method: 'POST', body: formData });
|
| 105 |
+
const result = await response.json();
|
| 106 |
+
if (result.success) { return result.data.url; }
|
| 107 |
+
else { throw new Error(result.error.message || 'Image upload failed.'); }
|
| 108 |
+
};
|
| 109 |
+
|
| 110 |
+
// --- Caching and State Management ---
|
| 111 |
+
const startNewChat = () => {
|
| 112 |
+
currentSessionId = null;
|
| 113 |
+
chatMessages.innerHTML = `<div class="welcome-message"><h1>EasyFarms Assistant</h1></div>`;
|
| 114 |
+
chatTitle.textContent = "New Chat";
|
| 115 |
+
updateActiveChatItem();
|
| 116 |
+
};
|
| 117 |
+
|
| 118 |
+
const switchChat = async (sessionId) => {
|
| 119 |
+
currentSessionId = sessionId;
|
| 120 |
+
chatMessages.innerHTML = '';
|
| 121 |
+
if (conversationsCache[sessionId] && conversationsCache[sessionId].messages) {
|
| 122 |
+
conversationsCache[sessionId].messages.forEach(displayMessage);
|
| 123 |
+
} else {
|
| 124 |
+
const loading = displayMessage({ role: 'assistant', content: 'Loading chat history...', isLoading: true });
|
| 125 |
+
try {
|
| 126 |
+
const response = await fetch(`/history/messages/${sessionId}`);
|
| 127 |
+
const messages = await response.json();
|
| 128 |
+
if (!conversationsCache[sessionId]) conversationsCache[sessionId] = {};
|
| 129 |
+
conversationsCache[sessionId].messages = messages;
|
| 130 |
+
saveCache();
|
| 131 |
+
chatMessages.removeChild(loading);
|
| 132 |
+
messages.forEach(displayMessage);
|
| 133 |
+
} catch (error) {
|
| 134 |
+
loading.innerHTML = marked.parse("Failed to load chat history.");
|
| 135 |
+
}
|
| 136 |
+
}
|
| 137 |
+
chatTitle.textContent = conversationsCache[sessionId]?.title || "Chat";
|
| 138 |
+
updateActiveChatItem();
|
| 139 |
+
closeSidebar();
|
| 140 |
+
};
|
| 141 |
+
|
| 142 |
+
const updateCache = (sessionId, userTurn, assistantTurn) => {
|
| 143 |
+
const isNewChat = !currentSessionId;
|
| 144 |
+
currentSessionId = sessionId;
|
| 145 |
+
if (isNewChat) {
|
| 146 |
+
const title = (userTurn.content || "Image Query").substring(0, 30) + '...';
|
| 147 |
+
conversationsCache[sessionId] = { title, messages: [] };
|
| 148 |
+
const item = document.createElement('div');
|
| 149 |
+
item.className = 'chat-history-item';
|
| 150 |
+
item.textContent = title;
|
| 151 |
+
item.dataset.sessionId = sessionId;
|
| 152 |
+
item.addEventListener('click', () => switchChat(sessionId));
|
| 153 |
+
chatHistoryList.prepend(item);
|
| 154 |
+
}
|
| 155 |
+
const userMessage = { role: 'user', content: userTurn.content };
|
| 156 |
+
if (userTurn.imageUrl) userMessage.imageUrl = userTurn.imageUrl;
|
| 157 |
+
const assistantMessage = { role: 'assistant', content: assistantTurn.content };
|
| 158 |
+
conversationsCache[sessionId].messages.push(userMessage, assistantMessage);
|
| 159 |
+
saveCache();
|
| 160 |
+
updateActiveChatItem();
|
| 161 |
+
};
|
| 162 |
+
|
| 163 |
+
const displayMessage = (message) => {
|
| 164 |
+
const { role, content, imageUrl, isLoading } = message;
|
| 165 |
+
const sender = role || message.sender;
|
| 166 |
+
const messageDiv = document.createElement('div');
|
| 167 |
+
messageDiv.classList.add('message', `${sender}-message`);
|
| 168 |
+
let htmlContent = '';
|
| 169 |
+
const imageSrc = (typeof imageUrl === 'object' && imageUrl instanceof File) ? URL.createObjectURL(imageUrl) : imageUrl;
|
| 170 |
+
if (imageSrc) { htmlContent += `<img src="${imageSrc}" alt="User upload" class="user-upload">`; }
|
| 171 |
+
if (content) { htmlContent += marked.parse(content); }
|
| 172 |
+
messageDiv.innerHTML = htmlContent || (isLoading ? '...' : '');
|
| 173 |
+
if (isLoading) messageDiv.classList.add('loading');
|
| 174 |
+
chatMessages.appendChild(messageDiv);
|
| 175 |
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
| 176 |
+
return messageDiv;
|
| 177 |
+
};
|
| 178 |
+
|
| 179 |
+
// --- Image Preview Handling ---
|
| 180 |
+
const handleImageSelect = (event) => {
|
| 181 |
+
const file = event.target.files[0];
|
| 182 |
+
if (file) {
|
| 183 |
+
selectedImageFile = file;
|
| 184 |
+
imagePreview.src = URL.createObjectURL(file);
|
| 185 |
+
imagePreviewContainer.style.display = 'block';
|
| 186 |
+
}
|
| 187 |
+
};
|
| 188 |
+
|
| 189 |
+
const removeSelectedImage = () => {
|
| 190 |
+
selectedImageFile = null;
|
| 191 |
+
imageUploadInput.value = '';
|
| 192 |
+
imagePreviewContainer.style.display = 'none';
|
| 193 |
+
imagePreview.src = '#';
|
| 194 |
+
};
|
| 195 |
+
|
| 196 |
+
// --- LocalStorage Cache & Sidebar Rendering ---
|
| 197 |
+
const saveCache = () => localStorage.setItem('easyfarms_cache', JSON.stringify(conversationsCache));
|
| 198 |
+
const loadCache = () => {
|
| 199 |
+
const saved = localStorage.getItem('easyfarms_cache');
|
| 200 |
+
if (saved) conversationsCache = JSON.parse(saved);
|
| 201 |
+
};
|
| 202 |
+
|
| 203 |
+
const renderChatHistoryFromAPI = async () => {
|
| 204 |
+
try {
|
| 205 |
+
const response = await fetch('/history/sessions');
|
| 206 |
+
const sessions = await response.json();
|
| 207 |
+
chatHistoryList.innerHTML = '';
|
| 208 |
+
sessions.reverse().forEach(session => {
|
| 209 |
+
if (!conversationsCache[session.session_id]) conversationsCache[session.session_id] = {};
|
| 210 |
+
conversationsCache[session.session_id].title = session.title;
|
| 211 |
+
const item = document.createElement('div');
|
| 212 |
+
item.className = 'chat-history-item';
|
| 213 |
+
item.textContent = session.title;
|
| 214 |
+
item.dataset.sessionId = session.session_id;
|
| 215 |
+
item.addEventListener('click', () => switchChat(session.session_id));
|
| 216 |
+
chatHistoryList.appendChild(item);
|
| 217 |
+
});
|
| 218 |
+
saveCache();
|
| 219 |
+
updateActiveChatItem();
|
| 220 |
+
} catch (error) { console.error("Failed to render chat history from API:", error); }
|
| 221 |
+
};
|
| 222 |
+
|
| 223 |
+
const updateActiveChatItem = () => {
|
| 224 |
+
document.querySelectorAll('.chat-history-item').forEach(item => {
|
| 225 |
+
item.classList.toggle('active', item.dataset.sessionId === currentSessionId);
|
| 226 |
+
});
|
| 227 |
+
};
|
| 228 |
+
|
| 229 |
+
const closeSidebar = () => appContainer.classList.remove('sidebar-visible');
|
| 230 |
+
|
| 231 |
+
// --- Start the Application ---
|
| 232 |
+
init();
|
| 233 |
+
});
|
static/style.css
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
--bg-color: #131314;
|
| 3 |
+
--sidebar-color: #1e1f20;
|
| 4 |
+
--chat-area-color: #131314;
|
| 5 |
+
--user-msg-color: #383a3f;
|
| 6 |
+
--assistant-msg-color: #252629;
|
| 7 |
+
--text-color: #e3e3e3;
|
| 8 |
+
--border-color: #333;
|
| 9 |
+
--accent-color: #4CAF50;
|
| 10 |
+
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 14 |
+
|
| 15 |
+
body {
|
| 16 |
+
font-family: var(--font-family);
|
| 17 |
+
background-color: var(--bg-color);
|
| 18 |
+
color: var(--text-color);
|
| 19 |
+
display: flex;
|
| 20 |
+
height: 100vh;
|
| 21 |
+
overflow: hidden;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
.app-container {
|
| 25 |
+
display: flex;
|
| 26 |
+
width: 100%;
|
| 27 |
+
height: 100%;
|
| 28 |
+
position: relative;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
/* Sidebar (hidden on mobile by default) */
|
| 32 |
+
.sidebar {
|
| 33 |
+
position: absolute;
|
| 34 |
+
top: 0;
|
| 35 |
+
left: 0;
|
| 36 |
+
bottom: 0;
|
| 37 |
+
width: 260px;
|
| 38 |
+
background-color: var(--sidebar-color);
|
| 39 |
+
display: flex;
|
| 40 |
+
flex-direction: column;
|
| 41 |
+
padding: 10px;
|
| 42 |
+
transform: translateX(-100%);
|
| 43 |
+
transition: transform 0.3s ease-in-out;
|
| 44 |
+
z-index: 101; /* Must be on top */
|
| 45 |
+
}
|
| 46 |
+
.app-container.sidebar-visible .sidebar {
|
| 47 |
+
transform: translateX(0);
|
| 48 |
+
}
|
| 49 |
+
.sidebar-header {
|
| 50 |
+
display: flex;
|
| 51 |
+
gap: 10px;
|
| 52 |
+
align-items: center;
|
| 53 |
+
}
|
| 54 |
+
.sidebar-button {
|
| 55 |
+
flex-grow: 1; /* Allow button to take available space */
|
| 56 |
+
padding: 10px 15px;
|
| 57 |
+
background-color: transparent;
|
| 58 |
+
border: 1px solid var(--border-color);
|
| 59 |
+
border-radius: 8px;
|
| 60 |
+
color: var(--text-color);
|
| 61 |
+
cursor: pointer;
|
| 62 |
+
font-size: 1rem;
|
| 63 |
+
display: flex;
|
| 64 |
+
align-items: center;
|
| 65 |
+
gap: 10px;
|
| 66 |
+
transition: background-color 0.2s;
|
| 67 |
+
}
|
| 68 |
+
.sidebar-button:hover { background-color: var(--user-msg-color); }
|
| 69 |
+
.chat-history { flex-grow: 1; overflow-y: auto; margin-top: 20px; }
|
| 70 |
+
|
| 71 |
+
.chat-history-item {
|
| 72 |
+
padding: 10px 15px;
|
| 73 |
+
margin-bottom: 5px;
|
| 74 |
+
border-radius: 6px;
|
| 75 |
+
cursor: pointer;
|
| 76 |
+
white-space: nowrap;
|
| 77 |
+
overflow: hidden;
|
| 78 |
+
text-overflow: ellipsis;
|
| 79 |
+
transition: background-color 0.2s;
|
| 80 |
+
}
|
| 81 |
+
.chat-history-item:hover { background-color: var(--user-msg-color); }
|
| 82 |
+
.chat-history-item.active { background-color: var(--user-msg-color); font-weight: bold; }
|
| 83 |
+
|
| 84 |
+
.close-sidebar-btn {
|
| 85 |
+
background: none;
|
| 86 |
+
border: none;
|
| 87 |
+
color: #999;
|
| 88 |
+
cursor: pointer;
|
| 89 |
+
display: none; /* Hidden by default */
|
| 90 |
+
padding: 5px;
|
| 91 |
+
}
|
| 92 |
+
.app-container.sidebar-visible .close-sidebar-btn {
|
| 93 |
+
display: block; /* Show when sidebar is visible */
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
/* Main Chat Area */
|
| 97 |
+
.chat-area {
|
| 98 |
+
width: 100%;
|
| 99 |
+
display: flex;
|
| 100 |
+
flex-direction: column;
|
| 101 |
+
background-color: var(--chat-area-color);
|
| 102 |
+
position: relative;
|
| 103 |
+
transition: filter 0.3s;
|
| 104 |
+
}
|
| 105 |
+
.app-container.sidebar-visible .chat-area::before {
|
| 106 |
+
content: '';
|
| 107 |
+
position: absolute;
|
| 108 |
+
top: 0;
|
| 109 |
+
left: 0;
|
| 110 |
+
right: 0;
|
| 111 |
+
bottom: 0;
|
| 112 |
+
background-color: rgba(0, 0, 0, 0.5);
|
| 113 |
+
z-index: 100;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
.chat-header { display: flex; align-items: center; padding: 10px; border-bottom: 1px solid var(--border-color); }
|
| 117 |
+
.menu-toggle { background: none; border: none; color: var(--text-color); cursor: pointer; }
|
| 118 |
+
#chat-title { margin-left: 15px; font-size: 1.1rem; }
|
| 119 |
+
|
| 120 |
+
.chat-messages { flex-grow: 1; overflow-y: auto; padding: 10px 20px; display: flex; flex-direction: column; }
|
| 121 |
+
.welcome-message { text-align: center; margin: auto; color: #888; }
|
| 122 |
+
|
| 123 |
+
.message { max-width: 90%; margin-bottom: 15px; padding: 10px 15px; border-radius: 12px; line-height: 1.6; word-wrap: break-word; }
|
| 124 |
+
.message p, .message ul, .message ol { margin: 0.5em 0; }
|
| 125 |
+
.message ul, .message ol { padding-left: 20px; }
|
| 126 |
+
.message code { background-color: #111; padding: 2px 4px; border-radius: 4px; }
|
| 127 |
+
.message pre { background-color: #111; padding: 10px; border-radius: 8px; overflow-x: auto; }
|
| 128 |
+
.message img.user-upload { max-width: 150px; border-radius: 8px; margin-top: 10px; }
|
| 129 |
+
|
| 130 |
+
.user-message { background-color: var(--user-msg-color); align-self: flex-end; }
|
| 131 |
+
.assistant-message { background-color: var(--assistant-msg-color); align-self: flex-start; }
|
| 132 |
+
.assistant-message.loading::after { content: '...'; display: inline-block; animation: blink 1s infinite; }
|
| 133 |
+
@keyframes blink { 50% { opacity: 0; } }
|
| 134 |
+
|
| 135 |
+
/* Chat Input */
|
| 136 |
+
.chat-input-area { padding: 10px 20px; border-top: 1px solid var(--border-color); }
|
| 137 |
+
.image-preview { position: relative; width: 80px; margin-bottom: 10px; }
|
| 138 |
+
#image-preview { width: 100%; border-radius: 8px; }
|
| 139 |
+
#remove-image-btn {
|
| 140 |
+
position: absolute; top: -5px; right: -5px;
|
| 141 |
+
background: #000; color: #fff; border: 1px solid #fff;
|
| 142 |
+
border-radius: 50%; width: 20px; height: 20px;
|
| 143 |
+
cursor: pointer; display: flex; align-items: center; justify-content: center;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.chat-input-wrapper { display: flex; align-items: flex-end; gap: 10px; }
|
| 147 |
+
#message-input {
|
| 148 |
+
flex-grow: 1; background: var(--assistant-msg-color);
|
| 149 |
+
border: 1px solid var(--border-color); color: var(--text-color);
|
| 150 |
+
font-size: 1rem; padding: 10px; resize: none;
|
| 151 |
+
max-height: 150px; overflow-y: auto; outline: none; border-radius: 8px;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.input-action-btn { background: transparent; border: none; color: #888; cursor: pointer; height: 44px; width: 44px; display: flex; align-items: center; justify-content: center; }
|
| 155 |
+
.send-btn { background: var(--accent-color); color: white; border-radius: 8px; }
|
| 156 |
+
|
| 157 |
+
/* Desktop styles */
|
| 158 |
+
@media (min-width: 768px) {
|
| 159 |
+
.chat-header { display: none; }
|
| 160 |
+
.sidebar { position: static; transform: translateX(0); border-right: 1px solid var(--border-color); }
|
| 161 |
+
.close-sidebar-btn { display: none !important; } /* Close button is never needed on desktop */
|
| 162 |
+
.app-container.sidebar-visible .chat-area::before { display: none; } /* No overlay on desktop */
|
| 163 |
+
.chat-area { width: auto; flex-grow: 1; }
|
| 164 |
+
}
|
templates/index.html
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>EasyFarms Assistant</title>
|
| 7 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
| 8 |
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
| 9 |
+
</head>
|
| 10 |
+
<body>
|
| 11 |
+
<div id="app-container" class="app-container">
|
| 12 |
+
<!-- Sidebar for sessions (hidden on mobile) -->
|
| 13 |
+
<aside class="sidebar">
|
| 14 |
+
<div class="sidebar-header">
|
| 15 |
+
<button id="new-chat-btn" class="sidebar-button">
|
| 16 |
+
<span class="icon">+</span> New Chat
|
| 17 |
+
</button>
|
| 18 |
+
<button id="close-sidebar-btn" class="close-sidebar-btn">
|
| 19 |
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="24" height="24">
|
| 20 |
+
<path fill-rule="evenodd" d="M5.47 5.47a.75.75 0 011.06 0L12 10.94l5.47-5.47a.75.75 0 111.06 1.06L13.06 12l5.47 5.47a.75.75 0 11-1.06 1.06L12 13.06l-5.47 5.47a.75.75 0 01-1.06-1.06L10.94 12 5.47 6.53a.75.75 0 010-1.06z" clip-rule="evenodd" />
|
| 21 |
+
</svg>
|
| 22 |
+
</button>
|
| 23 |
+
</div>
|
| 24 |
+
<div class="chat-history" id="chat-history-list"></div>
|
| 25 |
+
</aside>
|
| 26 |
+
|
| 27 |
+
<!-- Main chat window -->
|
| 28 |
+
<main id="chat-area" class="chat-area">
|
| 29 |
+
<header class="chat-header">
|
| 30 |
+
<button id="menu-toggle" class="menu-toggle">
|
| 31 |
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="24" height="24"><path d="M3 6.75A.75.75 0 013.75 6h16.5a.75.75 0 010 1.5H3.75A.75.75 0 013 6.75zM3 12a.75.75 0 01.75-.75h16.5a.75.75 0 010 1.5H3.75A.75.75 0 013 12zm0 5.25a.75.75 0 01.75-.75h16.5a.75.75 0 010 1.5H3.75a.75.75 0 01-.75-.75z"></path></svg>
|
| 32 |
+
</button>
|
| 33 |
+
<h2 id="chat-title">New Chat</h2>
|
| 34 |
+
</header>
|
| 35 |
+
<div class="chat-messages" id="chat-messages">
|
| 36 |
+
<div class="welcome-message">
|
| 37 |
+
<h1>EasyFarms Assistant</h1>
|
| 38 |
+
</div>
|
| 39 |
+
</div>
|
| 40 |
+
<div class="chat-input-area">
|
| 41 |
+
<div class="image-preview" id="image-preview-container" style="display: none;">
|
| 42 |
+
<img id="image-preview" src="#" alt="Image preview"/>
|
| 43 |
+
<button id="remove-image-btn">×</button>
|
| 44 |
+
</div>
|
| 45 |
+
<div class="chat-input-wrapper">
|
| 46 |
+
<input type="file" id="image-upload-input" accept="image/*" style="display: none;">
|
| 47 |
+
<button id="image-upload-btn" class="input-action-btn">
|
| 48 |
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="20" height="20"><path fill-rule="evenodd" d="M1.5 6a2.25 2.25 0 012.25-2.25h16.5A2.25 2.25 0 0122.5 6v12a2.25 2.25 0 01-2.25 2.25H3.75A2.25 2.25 0 011.5 18V6zM3 16.06V18c0 .414.336.75.75.75h16.5A.75.75 0 0021 18v-1.94l-2.69-2.689a1.5 1.5 0 00-2.12 0l-.88.879.97.97a.75.75 0 11-1.06 1.06l-5.16-5.159a1.5 1.5 0 00-2.12 0L3 16.061zm10.125-7.81a1.125 1.125 0 112.25 0 1.125 1.125 0 01-2.25 0z" clip-rule="evenodd"></path></svg>
|
| 49 |
+
</button>
|
| 50 |
+
<textarea id="message-input" placeholder="Ask a question..." rows="1"></textarea>
|
| 51 |
+
<button id="send-btn" class="input-action-btn send-btn">
|
| 52 |
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="send-icon"><path d="M3.478 2.405a.75.75 0 00-.926.94l2.432 7.905H13.5a.75.75 0 010 1.5H4.984l-2.432 7.905a.75.75 0 00.926.94 60.519 60.519 0 0018.445-8.986.75.75 0 000-1.218A60.517 60.517 0 003.478 2.405z"></path></svg>
|
| 53 |
+
</button>
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
</main>
|
| 57 |
+
</div>
|
| 58 |
+
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
| 59 |
+
</body>
|
| 60 |
+
</html>
|