NitinBot001 commited on
Commit
f374654
·
verified ·
1 Parent(s): 63ce3f8

Upload 10 files

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