code pushed'
Browse files- .gitignore +1 -0
- Dockerfile +18 -0
- ageents/__pycache__/advisoryAgent.cpython-313.pyc +0 -0
- ageents/__pycache__/marketAdvisoryAgent.cpython-313.pyc +0 -0
- ageents/__pycache__/marketAgent.cpython-312.pyc +0 -0
- ageents/__pycache__/marketAgent.cpython-313.pyc +0 -0
- ageents/__pycache__/predictionAgent.cpython-313.pyc +0 -0
- ageents/__pycache__/soilAgent.cpython-313.pyc +0 -0
- ageents/__pycache__/weatherAgent.cpython-313.pyc +0 -0
- ageents/advisoryAgent.py +346 -0
- ageents/ledgerAgent.py +91 -0
- ageents/marketAdvisoryAgent.py +483 -0
- ageents/marketAgent.py +525 -0
- ageents/predictionAgent.py +534 -0
- ageents/soilAgent.py +334 -0
- ageents/weatherAgent.py +237 -0
- firebase_config.py +18 -0
- firebase_key.json +13 -0
- main.py +2264 -0
- models/__init__.py +7 -0
- models/__pycache__/__init__.cpython-312.pyc +0 -0
- models/__pycache__/__init__.cpython-313.pyc +0 -0
- models/__pycache__/expense.cpython-312.pyc +0 -0
- models/__pycache__/expense.cpython-313.pyc +0 -0
- models/__pycache__/market.cpython-313.pyc +0 -0
- models/expense.py +36 -0
- models/market.py +100 -0
- requirements.txt +9 -0
- seed_market_data.py +248 -0
- soil_data_seeder.py +225 -0
.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
.env
|
Dockerfile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Base image
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
+
|
| 4 |
+
# Set work directory
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Install dependencies
|
| 8 |
+
COPY requirements.txt .
|
| 9 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 10 |
+
|
| 11 |
+
# Copy project files
|
| 12 |
+
COPY . .
|
| 13 |
+
|
| 14 |
+
# Expose the port Hugging Face expects
|
| 15 |
+
EXPOSE 7860
|
| 16 |
+
|
| 17 |
+
# Command to run FastAPI with uvicorn
|
| 18 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
ageents/__pycache__/advisoryAgent.cpython-313.pyc
ADDED
|
Binary file (15.1 kB). View file
|
|
|
ageents/__pycache__/marketAdvisoryAgent.cpython-313.pyc
ADDED
|
Binary file (17.7 kB). View file
|
|
|
ageents/__pycache__/marketAgent.cpython-312.pyc
ADDED
|
Binary file (19.2 kB). View file
|
|
|
ageents/__pycache__/marketAgent.cpython-313.pyc
ADDED
|
Binary file (20.5 kB). View file
|
|
|
ageents/__pycache__/predictionAgent.cpython-313.pyc
ADDED
|
Binary file (19.7 kB). View file
|
|
|
ageents/__pycache__/soilAgent.cpython-313.pyc
ADDED
|
Binary file (14.1 kB). View file
|
|
|
ageents/__pycache__/weatherAgent.cpython-313.pyc
ADDED
|
Binary file (10.9 kB). View file
|
|
|
ageents/advisoryAgent.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Advisory Agent - Transforms raw predictions into farmer-friendly advice
|
| 3 |
+
Location: agents/advisoryAgent.py
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import json
|
| 8 |
+
import asyncio
|
| 9 |
+
from dotenv import load_dotenv, find_dotenv
|
| 10 |
+
from pydantic import BaseModel
|
| 11 |
+
from typing import Dict, Any, List, Optional
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
|
| 14 |
+
from agents import (
|
| 15 |
+
Agent,
|
| 16 |
+
Runner,
|
| 17 |
+
AsyncOpenAI,
|
| 18 |
+
OpenAIChatCompletionsModel,
|
| 19 |
+
set_tracing_disabled,
|
| 20 |
+
SQLiteSession,
|
| 21 |
+
AgentOutputSchema,
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
from ageents.predictionAgent import run_prediction_workflow
|
| 25 |
+
|
| 26 |
+
# ---------------- Setup ----------------
|
| 27 |
+
load_dotenv(find_dotenv())
|
| 28 |
+
set_tracing_disabled(True)
|
| 29 |
+
|
| 30 |
+
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
| 31 |
+
|
| 32 |
+
if not GEMINI_API_KEY:
|
| 33 |
+
raise RuntimeError("Missing Gemini API key in .env -> GEMINI_API_KEY")
|
| 34 |
+
|
| 35 |
+
client_provider = AsyncOpenAI(
|
| 36 |
+
api_key=GEMINI_API_KEY,
|
| 37 |
+
base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
|
| 38 |
+
)
|
| 39 |
+
Model = OpenAIChatCompletionsModel(
|
| 40 |
+
model="gemini-2.0-flash",
|
| 41 |
+
openai_client=client_provider,
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
runner = Runner()
|
| 45 |
+
|
| 46 |
+
# ---------------- Output Schema ----------------
|
| 47 |
+
class AdvisoryOutput(BaseModel):
|
| 48 |
+
"""Farmer-friendly advisory response"""
|
| 49 |
+
|
| 50 |
+
# Header
|
| 51 |
+
farm_name: str
|
| 52 |
+
advisory_date: str
|
| 53 |
+
advisory_id: str
|
| 54 |
+
|
| 55 |
+
# Status Overview (Simple, visual)
|
| 56 |
+
status_emoji: str # ๐ข ๐ก ๐ด
|
| 57 |
+
status_message: str # "Everything looks great!" or "Attention needed"
|
| 58 |
+
confidence_level: str # "High", "Moderate", "Low"
|
| 59 |
+
|
| 60 |
+
# Plain Language Summary
|
| 61 |
+
situation_summary: str # 2-3 sentences explaining current conditions
|
| 62 |
+
what_it_means: str # What this means for your farm in simple terms
|
| 63 |
+
|
| 64 |
+
# Priority Actions (Grouped by urgency)
|
| 65 |
+
urgent_today: List[str] # Must do today/tomorrow
|
| 66 |
+
this_week: List[str] # Should do this week
|
| 67 |
+
plan_ahead: List[str] # Long-term planning
|
| 68 |
+
|
| 69 |
+
# Weather Guidance
|
| 70 |
+
weather_outlook: str # Simple weather explanation
|
| 71 |
+
weather_impact: str # How weather affects your crop
|
| 72 |
+
weather_tips: List[str] # Specific weather-related actions
|
| 73 |
+
|
| 74 |
+
# Soil & Nutrition Guidance
|
| 75 |
+
soil_health_status: str # Simple soil health explanation
|
| 76 |
+
nutrition_advice: str # What to feed your crops
|
| 77 |
+
soil_tips: List[str] # Specific soil-related actions
|
| 78 |
+
|
| 79 |
+
# Water Management
|
| 80 |
+
irrigation_guidance: str # When and how much to water
|
| 81 |
+
water_saving_tips: List[str] # Optional efficiency tips
|
| 82 |
+
|
| 83 |
+
# Pest & Disease Alert
|
| 84 |
+
pest_risk_level: str # "Low", "Moderate", "High"
|
| 85 |
+
pest_advice: str # What to watch for
|
| 86 |
+
prevention_steps: List[str] # How to prevent issues
|
| 87 |
+
|
| 88 |
+
# Harvest Planning
|
| 89 |
+
harvest_readiness: str # "Not yet", "Soon", "Ready"
|
| 90 |
+
harvest_timing: str # When to harvest
|
| 91 |
+
harvest_tips: List[str] # Pre-harvest checklist
|
| 92 |
+
|
| 93 |
+
# Economic Insights
|
| 94 |
+
yield_expectation: str # Expected yield in simple terms
|
| 95 |
+
cost_saving_opportunities: List[str] # Ways to save money
|
| 96 |
+
market_advice: Optional[str] # When to sell (if applicable)
|
| 97 |
+
|
| 98 |
+
# Success Factors
|
| 99 |
+
what_going_well: List[str] # Positive aspects
|
| 100 |
+
areas_to_improve: List[str] # Improvement opportunities
|
| 101 |
+
|
| 102 |
+
# Resources & Support
|
| 103 |
+
helpful_resources: List[str] # Links to guides, videos, etc.
|
| 104 |
+
contact_support: str # When to contact extension office
|
| 105 |
+
|
| 106 |
+
# Next Steps Summary
|
| 107 |
+
next_advisory_date: str # When to check back
|
| 108 |
+
key_reminders: List[str] # 3-5 most important things to remember
|
| 109 |
+
|
| 110 |
+
# ---------------- Advisory Agent ----------------
|
| 111 |
+
advisory_agent = Agent(
|
| 112 |
+
name="FarmerAdvisoryAgent",
|
| 113 |
+
model=Model,
|
| 114 |
+
instructions="""
|
| 115 |
+
You are an Agricultural Advisory Agent for farmers. Your role is to translate technical prediction data into clear, actionable, farmer-friendly advice.
|
| 116 |
+
|
| 117 |
+
Input: Raw prediction data with weather, soil, and risk information (JSON)
|
| 118 |
+
|
| 119 |
+
Your task:
|
| 120 |
+
1. Write in simple, conversational language (8th-grade reading level)
|
| 121 |
+
2. Use emojis strategically for visual clarity (๐ฑ โ๏ธ ๐ง โ ๏ธ โ
)
|
| 122 |
+
3. Group actions by urgency and priority
|
| 123 |
+
4. Explain WHY actions are needed, not just WHAT to do
|
| 124 |
+
5. Provide positive encouragement along with warnings
|
| 125 |
+
6. Focus on practical, affordable solutions
|
| 126 |
+
7. Consider resource constraints of small farmers
|
| 127 |
+
8. Use local/regional context when available
|
| 128 |
+
9. Avoid jargon - use everyday language
|
| 129 |
+
10. Make advice specific to the crop and location
|
| 130 |
+
|
| 131 |
+
Writing style:
|
| 132 |
+
- "Your wheat needs water this week" NOT "Irrigation required at 40mm intervals"
|
| 133 |
+
- "Heavy rain is coming - check drainage" NOT "Precipitation forecast: 75% probability"
|
| 134 |
+
- "Soil nitrogen is low - add urea fertilizer" NOT "N:P:K ratio suboptimal"
|
| 135 |
+
|
| 136 |
+
Tone: Supportive, practical, conversational, respectful
|
| 137 |
+
|
| 138 |
+
Output: Strict JSON matching AdvisoryOutput schema
|
| 139 |
+
|
| 140 |
+
Remember: Farmers are smart and experienced - respect their knowledge while providing new insights.
|
| 141 |
+
""",
|
| 142 |
+
output_type=AdvisoryOutput,
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
# ---------------- Helper Functions ----------------
|
| 146 |
+
def generate_advisory_id():
|
| 147 |
+
"""Generate unique advisory ID"""
|
| 148 |
+
return f"ADV-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
|
| 149 |
+
|
| 150 |
+
def get_status_emoji(risk_level: str) -> str:
|
| 151 |
+
"""Convert risk level to emoji"""
|
| 152 |
+
mapping = {
|
| 153 |
+
"low": "๐ข",
|
| 154 |
+
"moderate": "๐ก",
|
| 155 |
+
"high": "๐ ",
|
| 156 |
+
"critical": "๐ด"
|
| 157 |
+
}
|
| 158 |
+
return mapping.get(risk_level.lower(), "๐ก")
|
| 159 |
+
|
| 160 |
+
def get_status_message(risk_level: str, success_probability: float) -> str:
|
| 161 |
+
"""Generate status message"""
|
| 162 |
+
if risk_level == "low" and success_probability > 75:
|
| 163 |
+
return "Everything looks great! Keep up the good work."
|
| 164 |
+
elif risk_level == "moderate":
|
| 165 |
+
return "Attention needed in some areas, but manageable."
|
| 166 |
+
elif risk_level == "high":
|
| 167 |
+
return "Important actions required soon."
|
| 168 |
+
else:
|
| 169 |
+
return "Urgent attention needed - take action immediately."
|
| 170 |
+
|
| 171 |
+
def simplify_technical_language(text: str) -> str:
|
| 172 |
+
"""Convert technical terms to simple language"""
|
| 173 |
+
replacements = {
|
| 174 |
+
"precipitation": "rain",
|
| 175 |
+
"irrigation": "watering",
|
| 176 |
+
"fertilization": "feeding crops",
|
| 177 |
+
"pathogen": "disease",
|
| 178 |
+
"nitrogen deficiency": "low nitrogen",
|
| 179 |
+
"optimal": "best",
|
| 180 |
+
"implement": "start",
|
| 181 |
+
"monitor": "check",
|
| 182 |
+
"facilitate": "help",
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
result = text
|
| 186 |
+
for tech, simple in replacements.items():
|
| 187 |
+
result = result.replace(tech, simple)
|
| 188 |
+
|
| 189 |
+
return result
|
| 190 |
+
|
| 191 |
+
# ---------------- Main Advisory Function ----------------
|
| 192 |
+
async def run_advisory_agent(location: str, crop: str, city: Optional[str] = None):
|
| 193 |
+
"""
|
| 194 |
+
Generate farmer-friendly advisory from prediction data
|
| 195 |
+
"""
|
| 196 |
+
|
| 197 |
+
print("\n" + "="*70)
|
| 198 |
+
print("๐ GENERATING FARMER ADVISORY")
|
| 199 |
+
print("="*70)
|
| 200 |
+
print(f"๐ Location: {location}")
|
| 201 |
+
print(f"๐พ Crop: {crop}")
|
| 202 |
+
print(f"โฐ Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
| 203 |
+
print("="*70 + "\n")
|
| 204 |
+
|
| 205 |
+
# Step 1: Get raw prediction data
|
| 206 |
+
try:
|
| 207 |
+
prediction_data = await run_prediction_workflow(location, crop, city)
|
| 208 |
+
except Exception as e:
|
| 209 |
+
print(f"โ Failed to get prediction data: {e}")
|
| 210 |
+
return {"error": f"Prediction failed: {str(e)}"}
|
| 211 |
+
|
| 212 |
+
if "error" in prediction_data:
|
| 213 |
+
return prediction_data
|
| 214 |
+
|
| 215 |
+
# Step 2: Prepare message for advisory agent
|
| 216 |
+
advisory_context = {
|
| 217 |
+
"prediction_data": prediction_data,
|
| 218 |
+
"location": location,
|
| 219 |
+
"crop": crop,
|
| 220 |
+
"advisory_id": generate_advisory_id(),
|
| 221 |
+
"generation_time": datetime.now().isoformat()
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
message = json.dumps(advisory_context, ensure_ascii=False)
|
| 225 |
+
|
| 226 |
+
# Step 3: Run advisory agent
|
| 227 |
+
print("๐ค Generating farmer-friendly advisory...\n")
|
| 228 |
+
try:
|
| 229 |
+
resp = await runner.run(
|
| 230 |
+
advisory_agent,
|
| 231 |
+
message,
|
| 232 |
+
session=SQLiteSession("trace.db")
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
if hasattr(resp, "output"):
|
| 236 |
+
advisory_output = resp.output.model_dump()
|
| 237 |
+
elif hasattr(resp, "final_output"):
|
| 238 |
+
advisory_output = resp.final_output.model_dump()
|
| 239 |
+
else:
|
| 240 |
+
advisory_output = None
|
| 241 |
+
except Exception as e:
|
| 242 |
+
print(f"โ Advisory agent failed: {e}")
|
| 243 |
+
advisory_output = None
|
| 244 |
+
|
| 245 |
+
# Step 4: Fallback if agent fails
|
| 246 |
+
if not advisory_output:
|
| 247 |
+
print("โ ๏ธ Agent failed, generating fallback advisory...\n")
|
| 248 |
+
|
| 249 |
+
risk_level = prediction_data.get("risk_level", "moderate")
|
| 250 |
+
success_prob = prediction_data.get("success_probability", 70.0)
|
| 251 |
+
|
| 252 |
+
advisory_output = {
|
| 253 |
+
"farm_name": location,
|
| 254 |
+
"advisory_date": datetime.now().strftime("%B %d, %Y"),
|
| 255 |
+
"advisory_id": generate_advisory_id(),
|
| 256 |
+
|
| 257 |
+
"status_emoji": get_status_emoji(risk_level),
|
| 258 |
+
"status_message": get_status_message(risk_level, success_prob),
|
| 259 |
+
"confidence_level": "Moderate" if success_prob > 60 else "Low",
|
| 260 |
+
|
| 261 |
+
"situation_summary": simplify_technical_language(
|
| 262 |
+
prediction_data.get("executive_summary", "Conditions are being analyzed for your farm.")
|
| 263 |
+
),
|
| 264 |
+
"what_it_means": f"Your {crop} crop has a {success_prob}% chance of success. {get_status_message(risk_level, success_prob)}",
|
| 265 |
+
|
| 266 |
+
"urgent_today": [simplify_technical_language(a) for a in prediction_data.get("immediate_actions", [])[:3]],
|
| 267 |
+
"this_week": [simplify_technical_language(a) for a in prediction_data.get("short_term_actions", [])[:3]],
|
| 268 |
+
"plan_ahead": [simplify_technical_language(a) for a in prediction_data.get("long_term_actions", [])[:3]],
|
| 269 |
+
|
| 270 |
+
"weather_outlook": f"Rain chance: {prediction_data.get('weather_summary', {}).get('precipitation_chance', 0)}%",
|
| 271 |
+
"weather_impact": "Weather conditions will affect your watering needs.",
|
| 272 |
+
"weather_tips": prediction_data.get("weather_risks", [])[:3],
|
| 273 |
+
|
| 274 |
+
"soil_health_status": f"Soil health score: {prediction_data.get('soil_summary', {}).get('health_score', 'N/A')}/100",
|
| 275 |
+
"nutrition_advice": "Follow the fertilizer schedule below.",
|
| 276 |
+
"soil_tips": prediction_data.get("soil_risks", [])[:3],
|
| 277 |
+
|
| 278 |
+
"irrigation_guidance": prediction_data.get("irrigation_schedule", {}).get("frequency", "Check soil moisture daily"),
|
| 279 |
+
"water_saving_tips": ["Use drip irrigation", "Water early morning", "Mulch to retain moisture"],
|
| 280 |
+
|
| 281 |
+
"pest_risk_level": prediction_data.get("pest_disease_risk", "Monitor regularly"),
|
| 282 |
+
"pest_advice": "Watch for early signs of pests or disease.",
|
| 283 |
+
"prevention_steps": ["Inspect crops weekly", "Remove affected plants", "Keep field clean"],
|
| 284 |
+
|
| 285 |
+
"harvest_readiness": "Monitor crop maturity",
|
| 286 |
+
"harvest_timing": prediction_data.get("optimal_harvest_window", "Follow crop maturity indicators"),
|
| 287 |
+
"harvest_tips": ["Check moisture content", "Prepare storage", "Plan labor needs"],
|
| 288 |
+
|
| 289 |
+
"yield_expectation": prediction_data.get("yield_forecast", "Moderate yield expected"),
|
| 290 |
+
"cost_saving_opportunities": ["Optimize fertilizer use", "Efficient irrigation", "Preventive pest control"],
|
| 291 |
+
"market_advice": prediction_data.get("market_timing_advice"),
|
| 292 |
+
|
| 293 |
+
"what_going_well": prediction_data.get("weather_opportunities", []) + prediction_data.get("soil_opportunities", []),
|
| 294 |
+
"areas_to_improve": [f"Address {risk}" for risk in (prediction_data.get("weather_risks", []) + prediction_data.get("soil_risks", []))[:3]],
|
| 295 |
+
|
| 296 |
+
"helpful_resources": [
|
| 297 |
+
"Contact local agricultural extension office",
|
| 298 |
+
"Check weather forecasts daily",
|
| 299 |
+
"Join farmer WhatsApp groups"
|
| 300 |
+
],
|
| 301 |
+
"contact_support": "Contact support if you see unusual crop symptoms or need urgent help.",
|
| 302 |
+
|
| 303 |
+
"next_advisory_date": "Check back in 3-5 days for updated conditions",
|
| 304 |
+
"key_reminders": [
|
| 305 |
+
f"Risk level: {risk_level}",
|
| 306 |
+
f"Success probability: {success_prob}%",
|
| 307 |
+
"Follow urgent actions first"
|
| 308 |
+
]
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
# Step 5: Display advisory
|
| 312 |
+
print("="*70)
|
| 313 |
+
print(f"๐ FARMER ADVISORY - {advisory_output.get('advisory_date')}")
|
| 314 |
+
print("="*70)
|
| 315 |
+
print(f"{advisory_output.get('status_emoji')} STATUS: {advisory_output.get('status_message')}")
|
| 316 |
+
print(f"\n๐ SITUATION:")
|
| 317 |
+
print(f" {advisory_output.get('situation_summary')}")
|
| 318 |
+
print(f"\n๐ก WHAT IT MEANS:")
|
| 319 |
+
print(f" {advisory_output.get('what_it_means')}")
|
| 320 |
+
print(f"\n๐ด URGENT (Do Today/Tomorrow):")
|
| 321 |
+
for action in advisory_output.get('urgent_today', [])[:3]:
|
| 322 |
+
print(f" โข {action}")
|
| 323 |
+
print(f"\n๐
THIS WEEK:")
|
| 324 |
+
for action in advisory_output.get('this_week', [])[:3]:
|
| 325 |
+
print(f" โข {action}")
|
| 326 |
+
print(f"\n๐ง WATERING:")
|
| 327 |
+
print(f" {advisory_output.get('irrigation_guidance')}")
|
| 328 |
+
print(f"\n๐ PEST RISK: {advisory_output.get('pest_risk_level')}")
|
| 329 |
+
print(f" {advisory_output.get('pest_advice')}")
|
| 330 |
+
print("="*70 + "\n")
|
| 331 |
+
|
| 332 |
+
return advisory_output
|
| 333 |
+
|
| 334 |
+
# ---------------- Standalone Execution ----------------
|
| 335 |
+
if __name__ == "__main__":
|
| 336 |
+
print("๐ AgriLedger+ Farmer Advisory Agent")
|
| 337 |
+
print("=" * 70)
|
| 338 |
+
|
| 339 |
+
location_input = input("Enter farm location: ")
|
| 340 |
+
crop_input = input("Enter crop name: ")
|
| 341 |
+
city_input = input("Enter city for weather (press Enter to use location): ").strip() or None
|
| 342 |
+
|
| 343 |
+
result = asyncio.run(run_advisory_agent(location_input, crop_input, city_input))
|
| 344 |
+
|
| 345 |
+
print("\n๐ Full Advisory JSON:")
|
| 346 |
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
ageents/ledgerAgent.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import uuid
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from pydantic import BaseModel, Field
|
| 5 |
+
from typing import Optional
|
| 6 |
+
from openai import AsyncOpenAI
|
| 7 |
+
from agents import Agent, function_tool, Runner, trace, OpenAIChatCompletionsModel , RunConfig
|
| 8 |
+
from firebase_config import get_db_ref
|
| 9 |
+
from models.expense import EntryCreate, EntryResponse
|
| 10 |
+
from dotenv import load_dotenv
|
| 11 |
+
|
| 12 |
+
from openai import AsyncOpenAI
|
| 13 |
+
from agents import Agent, Runner, RunContextWrapper, function_tool, trace
|
| 14 |
+
from dotenv import load_dotenv
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
load_dotenv()
|
| 18 |
+
|
| 19 |
+
# -------------------- MODEL PROVIDER --------------------
|
| 20 |
+
gemini_api_key = os.getenv("GEMINI_API_KEY")
|
| 21 |
+
provider = AsyncOpenAI(
|
| 22 |
+
api_key=gemini_api_key,
|
| 23 |
+
base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
|
| 24 |
+
)
|
| 25 |
+
model = OpenAIChatCompletionsModel(
|
| 26 |
+
model="gemini-2.5-flash",
|
| 27 |
+
openai_client=provider,
|
| 28 |
+
)
|
| 29 |
+
config = RunConfig(
|
| 30 |
+
model=model, model_provider=provider, tracing_disabled=False
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
# ============================
|
| 34 |
+
# ๐น FUNCTION TOOL
|
| 35 |
+
# ============================
|
| 36 |
+
|
| 37 |
+
@function_tool()
|
| 38 |
+
def add_ledger_entry(entry: EntryCreate) -> EntryResponse:
|
| 39 |
+
"""
|
| 40 |
+
Add a ledger entry to Firebase using UUID for ID.
|
| 41 |
+
Collection name: 'ledger_entries'
|
| 42 |
+
"""
|
| 43 |
+
|
| 44 |
+
# ๐งพ Generate ID + Timestamp
|
| 45 |
+
entry_id = str(uuid.uuid4())
|
| 46 |
+
created_at = entry.date or datetime.utcnow().isoformat()
|
| 47 |
+
|
| 48 |
+
# ๐ Prepare entry data
|
| 49 |
+
entry_data = {
|
| 50 |
+
"entryType": entry.entryType,
|
| 51 |
+
"category": entry.category,
|
| 52 |
+
"amount": entry.amount,
|
| 53 |
+
"currency": entry.currency,
|
| 54 |
+
"paymentMethod": entry.paymentMethod,
|
| 55 |
+
"notes": entry.notes,
|
| 56 |
+
"createdAt": created_at,
|
| 57 |
+
"recordedBy": entry.recordedBy,
|
| 58 |
+
"deviceId": entry.deviceId,
|
| 59 |
+
"syncStatus": "synced",
|
| 60 |
+
"meta": entry.meta.dict() if entry.meta else None,
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
# ๐ฅ Save to Firebase (collection auto create)
|
| 64 |
+
db_ref = get_db_ref("ledger_entries")
|
| 65 |
+
db_ref.child(entry_id).set(entry_data)
|
| 66 |
+
|
| 67 |
+
# ๐ฆ Return structured response
|
| 68 |
+
return EntryResponse(
|
| 69 |
+
id=entry_id,
|
| 70 |
+
entryType=entry.entryType,
|
| 71 |
+
category=entry.category,
|
| 72 |
+
amount=entry.amount,
|
| 73 |
+
currency=entry.currency,
|
| 74 |
+
paymentMethod=entry.paymentMethod,
|
| 75 |
+
notes=entry.notes,
|
| 76 |
+
createdAt=created_at,
|
| 77 |
+
recordedBy=entry.recordedBy,
|
| 78 |
+
deviceId=entry.deviceId,
|
| 79 |
+
meta=entry.meta,
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
# ============================
|
| 83 |
+
# ๐น AGENT DEFINITION
|
| 84 |
+
# ============================
|
| 85 |
+
|
| 86 |
+
ledger_agent = Agent(
|
| 87 |
+
name="LedgerAgent",
|
| 88 |
+
instructions="You record user income or expense entries into Firebase using add_ledger_entry tool.",
|
| 89 |
+
tools=[add_ledger_entry],
|
| 90 |
+
|
| 91 |
+
)
|
ageents/marketAdvisoryAgent.py
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Market Advisory Agent - Transforms raw market data into farmer-friendly advice
|
| 3 |
+
Location: ageents/marketAdvisoryAgent.py
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import json
|
| 8 |
+
import asyncio
|
| 9 |
+
from dotenv import load_dotenv, find_dotenv
|
| 10 |
+
from pydantic import BaseModel
|
| 11 |
+
from typing import Dict, Any, List, Optional
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
|
| 14 |
+
from agents import (
|
| 15 |
+
Agent,
|
| 16 |
+
Runner,
|
| 17 |
+
AsyncOpenAI,
|
| 18 |
+
OpenAIChatCompletionsModel,
|
| 19 |
+
set_tracing_disabled,
|
| 20 |
+
SQLiteSession,
|
| 21 |
+
AgentOutputSchema,
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
from ageents.marketAgent import get_market_advice
|
| 25 |
+
|
| 26 |
+
# ---------------- Setup ----------------
|
| 27 |
+
load_dotenv(find_dotenv())
|
| 28 |
+
set_tracing_disabled(True)
|
| 29 |
+
|
| 30 |
+
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
| 31 |
+
|
| 32 |
+
if not GEMINI_API_KEY:
|
| 33 |
+
raise RuntimeError("Missing Gemini API key in .env -> GEMINI_API_KEY")
|
| 34 |
+
|
| 35 |
+
client_provider = AsyncOpenAI(
|
| 36 |
+
api_key=GEMINI_API_KEY,
|
| 37 |
+
base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
|
| 38 |
+
)
|
| 39 |
+
Model = OpenAIChatCompletionsModel(
|
| 40 |
+
model="gemini-2.0-flash",
|
| 41 |
+
openai_client=client_provider,
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
runner = Runner()
|
| 45 |
+
|
| 46 |
+
# ---------------- Output Schema ----------------
|
| 47 |
+
class MarketAdvisoryOutput(BaseModel):
|
| 48 |
+
"""Farmer-friendly market advisory response"""
|
| 49 |
+
|
| 50 |
+
# Header
|
| 51 |
+
crop: str
|
| 52 |
+
advisory_date: str
|
| 53 |
+
advisory_id: str
|
| 54 |
+
|
| 55 |
+
# Quick Decision
|
| 56 |
+
decision_emoji: str # ๐ฐ (sell) ๐ (buy) โณ (wait) ๐ (hold)
|
| 57 |
+
main_action: str # "Sell Now", "Wait for Better Prices", "Good Time to Buy Inputs"
|
| 58 |
+
urgency_level: str # "urgent", "soon", "no_rush", "flexible"
|
| 59 |
+
|
| 60 |
+
# Simple Explanation
|
| 61 |
+
what_happening: str # Plain language: what's going on in the market
|
| 62 |
+
why_it_matters: str # How this affects your money/farm
|
| 63 |
+
best_move: str # What you should do and why
|
| 64 |
+
|
| 65 |
+
# Price Information (Simple)
|
| 66 |
+
current_price_pkr: float
|
| 67 |
+
price_direction: str # "going up", "going down", "staying same"
|
| 68 |
+
price_change_description: str # "Increased by 150 rupees in 7 days"
|
| 69 |
+
is_good_price: bool # True if it's a good selling price
|
| 70 |
+
|
| 71 |
+
# Timing Advice
|
| 72 |
+
when_to_act: str # "This week", "Within 2 weeks", "No rush", "Wait 1 month"
|
| 73 |
+
seasonal_advice: str # What season means for prices
|
| 74 |
+
upcoming_events: List[str] # Things that might change prices soon
|
| 75 |
+
|
| 76 |
+
# Money Talk (Practical)
|
| 77 |
+
expected_income_per_acre: Optional[str] # "15,000 to 20,000 rupees per acre"
|
| 78 |
+
profit_potential: str # "Good profit expected", "Break even", "Low profit"
|
| 79 |
+
price_forecast_simple: str # "Likely to rise", "May drop soon", "Should stay stable"
|
| 80 |
+
|
| 81 |
+
# Action Steps
|
| 82 |
+
do_today: List[str] # Immediate actions
|
| 83 |
+
do_this_week: List[str] # Short-term actions
|
| 84 |
+
plan_ahead: List[str] # Future planning
|
| 85 |
+
|
| 86 |
+
# Risk Warnings
|
| 87 |
+
risks_to_watch: List[str] # What could go wrong
|
| 88 |
+
how_to_protect: List[str] # How to reduce risks
|
| 89 |
+
|
| 90 |
+
# Alternative Options
|
| 91 |
+
if_cant_sell_now: List[str] # What if you can't sell right away
|
| 92 |
+
other_opportunities: List[str] # Other ways to make money from this crop
|
| 93 |
+
|
| 94 |
+
# Market Context (Simple)
|
| 95 |
+
supply_situation: str # "High supply", "Normal", "Low supply"
|
| 96 |
+
demand_situation: str # "High demand", "Normal", "Low demand"
|
| 97 |
+
competition_level: str # "Many sellers", "Normal", "Few sellers"
|
| 98 |
+
|
| 99 |
+
# Practical Tips
|
| 100 |
+
selling_tips: List[str] # How to get best price when selling
|
| 101 |
+
buying_tips: List[str] # How to save money when buying inputs
|
| 102 |
+
storage_advice: Optional[str] # If you should store crop instead of selling
|
| 103 |
+
|
| 104 |
+
# Comparison
|
| 105 |
+
compared_to_last_week: str # "Prices are 200 rupees higher"
|
| 106 |
+
compared_to_last_month: str # "Down from peak of 2,800 rupees"
|
| 107 |
+
compared_to_normal: str # "This is above average price"
|
| 108 |
+
|
| 109 |
+
# Transport & Logistics
|
| 110 |
+
market_access_tips: List[str] # How to get to market, which market to choose
|
| 111 |
+
transport_costs: Optional[str] # Expected transport costs
|
| 112 |
+
|
| 113 |
+
# Negotiation Help
|
| 114 |
+
fair_price_range: str # "2,400 to 2,600 rupees per 40kg"
|
| 115 |
+
dont_sell_below: float # Minimum acceptable price
|
| 116 |
+
aim_to_sell_at: float # Target selling price
|
| 117 |
+
|
| 118 |
+
# Community Info
|
| 119 |
+
what_neighbors_doing: Optional[str] # What other farmers are doing
|
| 120 |
+
cooperative_opportunities: List[str] # Benefits of selling together
|
| 121 |
+
|
| 122 |
+
# Resources
|
| 123 |
+
where_to_check_prices: List[str] # Where to verify current prices
|
| 124 |
+
who_to_contact: List[str] # Extension office, buyer contacts
|
| 125 |
+
|
| 126 |
+
# Summary
|
| 127 |
+
bottom_line: str # One-sentence takeaway
|
| 128 |
+
confidence_level: str # "Very confident", "Fairly confident", "Uncertain"
|
| 129 |
+
|
| 130 |
+
# ---------------- Market Advisory Agent ----------------
|
| 131 |
+
market_advisory_agent = Agent(
|
| 132 |
+
name="FarmerMarketAdvisoryAgent",
|
| 133 |
+
model=Model,
|
| 134 |
+
instructions="""
|
| 135 |
+
You are a Market Advisory Agent for Pakistani farmers. Transform technical market analysis into practical, money-focused advice.
|
| 136 |
+
|
| 137 |
+
Input: Raw market analysis with prices, trends, and technical data
|
| 138 |
+
|
| 139 |
+
Your task:
|
| 140 |
+
1. Translate market data into simple rupee amounts and percentages
|
| 141 |
+
2. Give clear YES/NO/WAIT advice on selling or buying
|
| 142 |
+
3. Explain market conditions like you're talking to a neighbor
|
| 143 |
+
4. Focus on MONEY - how much they can earn or save
|
| 144 |
+
5. Provide specific timing advice - "this week", "wait 2 weeks", etc.
|
| 145 |
+
6. Consider farmer's practical constraints (transport, storage, immediate cash needs)
|
| 146 |
+
7. Use local context - Pakistani markets, seasonal patterns, religious holidays
|
| 147 |
+
8. Give negotiation tips - minimum price to accept, target price to aim for
|
| 148 |
+
|
| 149 |
+
Writing style:
|
| 150 |
+
- "Sell now - prices are 200 rupees higher than last week" NOT "Upward trend of 8.3%"
|
| 151 |
+
- "You can earn 15,000-20,000 rupees per acre" NOT "ROI improvement observed"
|
| 152 |
+
- "Wait 2 weeks - harvest season will bring more buyers" NOT "Market dynamics suggest delayed transaction"
|
| 153 |
+
- "Don't sell below 2,400 rupees per bag" NOT "Maintain price floor at modal rate"
|
| 154 |
+
|
| 155 |
+
Money focus:
|
| 156 |
+
- Always convert percentages to actual rupee amounts
|
| 157 |
+
- Show expected income per acre when possible
|
| 158 |
+
- Compare to costs - "Price covers fertilizer + labor costs with 30% profit"
|
| 159 |
+
- Mention break-even points - "You need at least 2,200 rupees to cover costs"
|
| 160 |
+
|
| 161 |
+
Practical considerations:
|
| 162 |
+
- Transport costs (50-100 rupees per bag typical)
|
| 163 |
+
- Market timing (avoid selling Friday afternoon during prayer time)
|
| 164 |
+
- Storage options (if prices might rise soon)
|
| 165 |
+
- Bulk selling benefits (better price when selling with neighbors)
|
| 166 |
+
- Seasonal festivals (Eid, weddings increase demand for certain crops)
|
| 167 |
+
|
| 168 |
+
Tone: Friendly neighbor, practical businessman, supportive advisor
|
| 169 |
+
|
| 170 |
+
Output: Strict JSON matching MarketAdvisoryOutput schema
|
| 171 |
+
""",
|
| 172 |
+
output_type=AgentOutputSchema(MarketAdvisoryOutput, strict_json_schema=False),
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
# ---------------- Helper Functions ----------------
|
| 176 |
+
def generate_market_advisory_id():
|
| 177 |
+
"""Generate unique market advisory ID"""
|
| 178 |
+
return f"MKT-ADV-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
|
| 179 |
+
|
| 180 |
+
def get_decision_emoji(action: str) -> str:
|
| 181 |
+
"""Get emoji based on main action"""
|
| 182 |
+
action_lower = action.lower()
|
| 183 |
+
if "sell" in action_lower and "now" in action_lower:
|
| 184 |
+
return "๐ฐ"
|
| 185 |
+
elif "buy" in action_lower:
|
| 186 |
+
return "๐"
|
| 187 |
+
elif "wait" in action_lower:
|
| 188 |
+
return "โณ"
|
| 189 |
+
else:
|
| 190 |
+
return "๐"
|
| 191 |
+
|
| 192 |
+
def extract_price_from_advice(raw_advice: str) -> float:
|
| 193 |
+
"""Extract current price from raw market advice"""
|
| 194 |
+
import re
|
| 195 |
+
# Look for patterns like "โจ2,500" or "PKR 2500"
|
| 196 |
+
matches = re.findall(r'[โจPKR\s]*(\d{1,3}(?:,\d{3})*(?:\.\d{2})?)', raw_advice)
|
| 197 |
+
if matches:
|
| 198 |
+
# Take first number that looks like a price (usually 1000+)
|
| 199 |
+
for match in matches:
|
| 200 |
+
price = float(match.replace(',', ''))
|
| 201 |
+
if price > 100: # Reasonable crop price
|
| 202 |
+
return price
|
| 203 |
+
return 0.0
|
| 204 |
+
|
| 205 |
+
def simplify_trend(trend_text: str) -> dict:
|
| 206 |
+
"""Convert technical trend to simple description"""
|
| 207 |
+
trend_lower = trend_text.lower()
|
| 208 |
+
|
| 209 |
+
if "rising" in trend_lower or "increase" in trend_lower or "up" in trend_lower:
|
| 210 |
+
return {
|
| 211 |
+
"direction": "going up",
|
| 212 |
+
"is_good": True,
|
| 213 |
+
"simple": "Prices are rising - good for sellers!"
|
| 214 |
+
}
|
| 215 |
+
elif "falling" in trend_lower or "decrease" in trend_lower or "down" in trend_lower:
|
| 216 |
+
return {
|
| 217 |
+
"direction": "going down",
|
| 218 |
+
"is_good": False,
|
| 219 |
+
"simple": "Prices are dropping - not ideal for selling now"
|
| 220 |
+
}
|
| 221 |
+
else:
|
| 222 |
+
return {
|
| 223 |
+
"direction": "staying same",
|
| 224 |
+
"is_good": None,
|
| 225 |
+
"simple": "Prices are stable - no rush to decide"
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
# ---------------- Main Advisory Function ----------------
|
| 229 |
+
async def run_market_advisory(crop: str, farmer_context: Optional[Dict] = None):
|
| 230 |
+
"""
|
| 231 |
+
Generate farmer-friendly market advisory
|
| 232 |
+
"""
|
| 233 |
+
|
| 234 |
+
print("\n" + "="*70)
|
| 235 |
+
print("๐ฐ GENERATING MARKET ADVISORY")
|
| 236 |
+
print("="*70)
|
| 237 |
+
print(f"๐พ Crop: {crop.upper()}")
|
| 238 |
+
if farmer_context:
|
| 239 |
+
print(f"๐จโ๐พ Farmer Context: {farmer_context}")
|
| 240 |
+
print(f"โฐ Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
| 241 |
+
print("="*70 + "\n")
|
| 242 |
+
|
| 243 |
+
# Step 1: Get raw market analysis from market agent
|
| 244 |
+
try:
|
| 245 |
+
raw_advice = await get_market_advice(crop.lower(), farmer_context)
|
| 246 |
+
except Exception as e:
|
| 247 |
+
print(f"โ Failed to get market data: {e}")
|
| 248 |
+
return {"error": f"Market data unavailable: {str(e)}"}
|
| 249 |
+
|
| 250 |
+
if not raw_advice or "ERROR" in raw_advice:
|
| 251 |
+
return {"error": "Market analysis failed or database not seeded"}
|
| 252 |
+
|
| 253 |
+
# Step 2: Prepare context for advisory agent
|
| 254 |
+
advisory_context = {
|
| 255 |
+
"crop": crop,
|
| 256 |
+
"raw_market_analysis": raw_advice,
|
| 257 |
+
"farmer_context": farmer_context or {},
|
| 258 |
+
"advisory_id": generate_market_advisory_id(),
|
| 259 |
+
"generation_time": datetime.now().isoformat()
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
message = json.dumps(advisory_context, ensure_ascii=False)
|
| 263 |
+
|
| 264 |
+
# Step 3: Run market advisory agent
|
| 265 |
+
print("๐ค Transforming market data into farmer advice...\n")
|
| 266 |
+
try:
|
| 267 |
+
resp = await runner.run(
|
| 268 |
+
market_advisory_agent,
|
| 269 |
+
message,
|
| 270 |
+
session=SQLiteSession("trace.db")
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
if hasattr(resp, "output"):
|
| 274 |
+
advisory_output = resp.output.model_dump()
|
| 275 |
+
elif hasattr(resp, "final_output"):
|
| 276 |
+
advisory_output = resp.final_output.model_dump()
|
| 277 |
+
else:
|
| 278 |
+
advisory_output = None
|
| 279 |
+
except Exception as e:
|
| 280 |
+
print(f"โ Market advisory agent failed: {e}")
|
| 281 |
+
advisory_output = None
|
| 282 |
+
|
| 283 |
+
# Step 4: Fallback if agent fails
|
| 284 |
+
if not advisory_output:
|
| 285 |
+
print("โ ๏ธ Agent failed, generating fallback advisory...\n")
|
| 286 |
+
|
| 287 |
+
# Extract basic info from raw advice
|
| 288 |
+
current_price = extract_price_from_advice(raw_advice)
|
| 289 |
+
|
| 290 |
+
# Determine main action from raw advice
|
| 291 |
+
if "SELL NOW" in raw_advice.upper():
|
| 292 |
+
main_action = "Sell Now"
|
| 293 |
+
urgency = "urgent"
|
| 294 |
+
emoji = "๐ฐ"
|
| 295 |
+
elif "BUY" in raw_advice.upper():
|
| 296 |
+
main_action = "Good Time to Buy Inputs"
|
| 297 |
+
urgency = "soon"
|
| 298 |
+
emoji = "๐"
|
| 299 |
+
elif "WAIT" in raw_advice.upper():
|
| 300 |
+
main_action = "Wait for Better Prices"
|
| 301 |
+
urgency = "flexible"
|
| 302 |
+
emoji = "โณ"
|
| 303 |
+
else:
|
| 304 |
+
main_action = "Hold and Monitor"
|
| 305 |
+
urgency = "no_rush"
|
| 306 |
+
emoji = "๐"
|
| 307 |
+
|
| 308 |
+
advisory_output = {
|
| 309 |
+
"crop": crop,
|
| 310 |
+
"advisory_date": datetime.now().strftime("%B %d, %Y"),
|
| 311 |
+
"advisory_id": generate_market_advisory_id(),
|
| 312 |
+
|
| 313 |
+
"decision_emoji": emoji,
|
| 314 |
+
"main_action": main_action,
|
| 315 |
+
"urgency_level": urgency,
|
| 316 |
+
|
| 317 |
+
"what_happening": f"Market analysis shows current prices for {crop}.",
|
| 318 |
+
"why_it_matters": "Price movements affect your potential earnings from selling or costs for buying inputs.",
|
| 319 |
+
"best_move": main_action,
|
| 320 |
+
|
| 321 |
+
"current_price_pkr": current_price if current_price > 0 else 0.0,
|
| 322 |
+
"price_direction": "Check raw analysis for details",
|
| 323 |
+
"price_change_description": "Price trend data available in detailed analysis",
|
| 324 |
+
"is_good_price": "SELL" in main_action,
|
| 325 |
+
|
| 326 |
+
"when_to_act": "Within 1 week" if urgency == "urgent" else "Within 2-3 weeks" if urgency == "soon" else "Monitor market",
|
| 327 |
+
"seasonal_advice": f"Consider seasonal patterns for {crop} in your region",
|
| 328 |
+
"upcoming_events": ["Monitor local market conditions", "Check weather forecasts"],
|
| 329 |
+
|
| 330 |
+
"expected_income_per_acre": "Consult local buyers for current rates",
|
| 331 |
+
"profit_potential": "Depends on your production costs",
|
| 332 |
+
"price_forecast_simple": "Based on current trend: " + main_action,
|
| 333 |
+
|
| 334 |
+
"do_today": [
|
| 335 |
+
f"Check current {crop} prices at your local market",
|
| 336 |
+
"Calculate your break-even price (costs + transport)",
|
| 337 |
+
"Contact 2-3 buyers to compare offers"
|
| 338 |
+
],
|
| 339 |
+
|
| 340 |
+
"do_this_week": [
|
| 341 |
+
"Monitor price changes daily",
|
| 342 |
+
"Prepare crop for market (cleaning, grading)",
|
| 343 |
+
"Arrange transport if selling"
|
| 344 |
+
],
|
| 345 |
+
|
| 346 |
+
"plan_ahead": [
|
| 347 |
+
f"Track {crop} price patterns for future planning",
|
| 348 |
+
"Consider storage options for better timing",
|
| 349 |
+
"Join farmer cooperative for better prices"
|
| 350 |
+
],
|
| 351 |
+
|
| 352 |
+
"risks_to_watch": [
|
| 353 |
+
"Sudden price drops if oversupply",
|
| 354 |
+
"Weather affecting transport",
|
| 355 |
+
"Quality deterioration if stored too long"
|
| 356 |
+
],
|
| 357 |
+
|
| 358 |
+
"how_to_protect": [
|
| 359 |
+
"Don't wait too long if advised to sell",
|
| 360 |
+
"Store properly if holding crop",
|
| 361 |
+
"Have backup buyers identified"
|
| 362 |
+
],
|
| 363 |
+
|
| 364 |
+
"if_cant_sell_now": [
|
| 365 |
+
"Ensure proper storage facilities",
|
| 366 |
+
"Monitor prices weekly",
|
| 367 |
+
"Consider partial selling (sell some, hold some)"
|
| 368 |
+
],
|
| 369 |
+
|
| 370 |
+
"other_opportunities": [
|
| 371 |
+
"Direct sale to consumers (better margin)",
|
| 372 |
+
"Contract farming for next season",
|
| 373 |
+
"Value addition (processing, packaging)"
|
| 374 |
+
],
|
| 375 |
+
|
| 376 |
+
"supply_situation": "Check with local traders",
|
| 377 |
+
"demand_situation": "Monitor market activity",
|
| 378 |
+
"competition_level": "Observe number of sellers at market",
|
| 379 |
+
|
| 380 |
+
"selling_tips": [
|
| 381 |
+
"Sell early in morning for better prices",
|
| 382 |
+
"Grade your crop properly",
|
| 383 |
+
"Don't accept first offer - negotiate",
|
| 384 |
+
"Sell in bulk with neighbors for better rate"
|
| 385 |
+
],
|
| 386 |
+
|
| 387 |
+
"buying_tips": [
|
| 388 |
+
"Buy inputs off-season for lower prices",
|
| 389 |
+
"Purchase in bulk with cooperative",
|
| 390 |
+
"Compare suppliers before buying"
|
| 391 |
+
],
|
| 392 |
+
|
| 393 |
+
"storage_advice": "Store only if advised to wait and you have proper facilities",
|
| 394 |
+
|
| 395 |
+
"compared_to_last_week": "Check detailed analysis",
|
| 396 |
+
"compared_to_last_month": "Historical data shows trends",
|
| 397 |
+
"compared_to_normal": "Current price relative to seasonal average",
|
| 398 |
+
|
| 399 |
+
"market_access_tips": [
|
| 400 |
+
"Visit market on active trading days (avoid Fridays afternoon)",
|
| 401 |
+
"Compare prices at 2-3 different markets if possible",
|
| 402 |
+
"Consider online platforms for price discovery"
|
| 403 |
+
],
|
| 404 |
+
|
| 405 |
+
"transport_costs": "Estimate 50-100 rupees per 40kg bag for local transport",
|
| 406 |
+
|
| 407 |
+
"fair_price_range": f"Check current rates - target above {current_price if current_price > 0 else 'market rate'}",
|
| 408 |
+
"dont_sell_below": current_price * 0.9 if current_price > 0 else 0.0,
|
| 409 |
+
"aim_to_sell_at": current_price * 1.05 if current_price > 0 else 0.0,
|
| 410 |
+
|
| 411 |
+
"what_neighbors_doing": "Talk to other farmers about their selling plans",
|
| 412 |
+
"cooperative_opportunities": [
|
| 413 |
+
"Better prices through bulk selling",
|
| 414 |
+
"Shared transport costs",
|
| 415 |
+
"Collective bargaining power"
|
| 416 |
+
],
|
| 417 |
+
|
| 418 |
+
"where_to_check_prices": [
|
| 419 |
+
"Local agricultural market",
|
| 420 |
+
"District agriculture office",
|
| 421 |
+
"Online price portals",
|
| 422 |
+
"SMS price services"
|
| 423 |
+
],
|
| 424 |
+
|
| 425 |
+
"who_to_contact": [
|
| 426 |
+
"District Agriculture Extension Officer",
|
| 427 |
+
"Local market committee",
|
| 428 |
+
"Farmer cooperatives"
|
| 429 |
+
],
|
| 430 |
+
|
| 431 |
+
"bottom_line": main_action + " - Monitor market closely and act based on your cash needs.",
|
| 432 |
+
"confidence_level": "Fairly confident based on available data"
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
# Step 5: Display advisory
|
| 436 |
+
print("="*70)
|
| 437 |
+
print(f"๐ฐ MARKET ADVISORY - {advisory_output.get('crop', crop).upper()}")
|
| 438 |
+
print("="*70)
|
| 439 |
+
print(f"{advisory_output.get('decision_emoji')} DECISION: {advisory_output.get('main_action')}")
|
| 440 |
+
print(f"โก Urgency: {advisory_output.get('urgency_level').upper()}")
|
| 441 |
+
print(f"\n๐ต CURRENT PRICE: โจ{advisory_output.get('current_price_pkr', 0):,.2f} per 40kg")
|
| 442 |
+
print(f"๐ PRICE TREND: {advisory_output.get('price_direction')}")
|
| 443 |
+
print(f"\n๐ก WHAT'S HAPPENING:")
|
| 444 |
+
print(f" {advisory_output.get('what_happening')}")
|
| 445 |
+
print(f"\n๐ฏ BEST MOVE:")
|
| 446 |
+
print(f" {advisory_output.get('best_move')}")
|
| 447 |
+
print(f"\n๐
WHEN TO ACT: {advisory_output.get('when_to_act')}")
|
| 448 |
+
print(f"\nโ
DO TODAY:")
|
| 449 |
+
for action in advisory_output.get('do_today', [])[:3]:
|
| 450 |
+
print(f" โข {action}")
|
| 451 |
+
print(f"\nโ ๏ธ RISKS TO WATCH:")
|
| 452 |
+
for risk in advisory_output.get('risks_to_watch', [])[:3]:
|
| 453 |
+
print(f" โข {risk}")
|
| 454 |
+
print(f"\n๐ฌ BOTTOM LINE:")
|
| 455 |
+
print(f" {advisory_output.get('bottom_line')}")
|
| 456 |
+
print("="*70 + "\n")
|
| 457 |
+
|
| 458 |
+
return advisory_output
|
| 459 |
+
|
| 460 |
+
# ---------------- Standalone Execution ----------------
|
| 461 |
+
if __name__ == "__main__":
|
| 462 |
+
print("๐ฐ AgriLedger+ Market Advisory Agent")
|
| 463 |
+
print("=" * 70)
|
| 464 |
+
|
| 465 |
+
crop_input = input("Enter crop name: ")
|
| 466 |
+
|
| 467 |
+
# Optional farmer context
|
| 468 |
+
budget_input = input("Enter budget (PKR) [press Enter to skip]: ").strip()
|
| 469 |
+
land_input = input("Enter land size (acres) [press Enter to skip]: ").strip()
|
| 470 |
+
risk_input = input("Enter risk tolerance (low/medium/high) [press Enter to skip]: ").strip()
|
| 471 |
+
|
| 472 |
+
farmer_context = {}
|
| 473 |
+
if budget_input:
|
| 474 |
+
farmer_context["budget"] = float(budget_input)
|
| 475 |
+
if land_input:
|
| 476 |
+
farmer_context["land_size"] = float(land_input)
|
| 477 |
+
if risk_input:
|
| 478 |
+
farmer_context["risk_tolerance"] = risk_input
|
| 479 |
+
|
| 480 |
+
result = asyncio.run(run_market_advisory(crop_input, farmer_context or None))
|
| 481 |
+
|
| 482 |
+
print("\n๐ Full Advisory JSON:")
|
| 483 |
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
ageents/marketAgent.py
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Market Agent for AgriLedger+ - Uses Seeded Firebase Data Only
|
| 3 |
+
Fixed version with proper tool implementation
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
from datetime import datetime, timedelta
|
| 8 |
+
from typing import Dict, List, Optional, Any
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
from firebase_config import get_db_ref
|
| 11 |
+
from agents import Agent, function_tool, OpenAIChatCompletionsModel, Runner
|
| 12 |
+
from openai import AsyncOpenAI
|
| 13 |
+
import statistics
|
| 14 |
+
|
| 15 |
+
load_dotenv()
|
| 16 |
+
|
| 17 |
+
# -------------------- MODEL PROVIDER --------------------
|
| 18 |
+
gemini_api_key = os.getenv("GEMINI_API_KEY")
|
| 19 |
+
provider = AsyncOpenAI(
|
| 20 |
+
api_key=gemini_api_key,
|
| 21 |
+
base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
|
| 22 |
+
)
|
| 23 |
+
model = OpenAIChatCompletionsModel(
|
| 24 |
+
model="gemini-2.5-flash",
|
| 25 |
+
openai_client=provider,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# -------------------- CONSTANTS --------------------
|
| 29 |
+
VOLATILITY_THRESHOLD = 5.0
|
| 30 |
+
SIGNIFICANT_CHANGE = 10.0
|
| 31 |
+
|
| 32 |
+
# Pakistan crop seasons
|
| 33 |
+
PAKISTAN_CROP_SEASONS = {
|
| 34 |
+
"wheat": {"plant": [10, 11, 12], "harvest": [4, 5]},
|
| 35 |
+
"rice": {"plant": [5, 6, 7], "harvest": [10, 11]},
|
| 36 |
+
"maize": {"plant": [2, 3, 7, 8], "harvest": [6, 11]},
|
| 37 |
+
"cotton": {"plant": [4, 5, 6], "harvest": [9, 10, 11]},
|
| 38 |
+
"sugarcane": {"plant": [9, 10, 11], "harvest": [11, 12, 1, 2]},
|
| 39 |
+
"potato": {"plant": [1, 2, 9, 10], "harvest": [4, 5, 11, 12]},
|
| 40 |
+
"onion": {"plant": [10, 11, 12], "harvest": [3, 4, 5]},
|
| 41 |
+
"tomato": {"plant": [1, 2, 7, 8], "harvest": [5, 6, 11, 12]},
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
# -------------------- IMPLEMENTATION FUNCTIONS --------------------
|
| 45 |
+
# These are the actual implementations that do the work
|
| 46 |
+
|
| 47 |
+
def get_current_prices_impl(crop: Optional[str] = None) -> dict[str, Any]:
|
| 48 |
+
"""Get current market prices from Firebase seeded data"""
|
| 49 |
+
try:
|
| 50 |
+
latest_ref = get_db_ref("market_data/latest")
|
| 51 |
+
latest_data = latest_ref.get()
|
| 52 |
+
|
| 53 |
+
if not latest_data:
|
| 54 |
+
prices_ref = get_db_ref("market_data/prices")
|
| 55 |
+
all_prices = prices_ref.get() or {}
|
| 56 |
+
|
| 57 |
+
if not all_prices:
|
| 58 |
+
return {"error": "No market data found. Database needs to be seeded.", "requires_seeding": True}
|
| 59 |
+
|
| 60 |
+
latest_prices = {}
|
| 61 |
+
for crop_name, dates in all_prices.items():
|
| 62 |
+
if isinstance(dates, dict):
|
| 63 |
+
sorted_dates = sorted(dates.keys(), reverse=True)
|
| 64 |
+
if sorted_dates:
|
| 65 |
+
recent_date = sorted_dates[0]
|
| 66 |
+
recent_entries = dates[recent_date]
|
| 67 |
+
if isinstance(recent_entries, dict):
|
| 68 |
+
prices = [e["modal_price"] for e in recent_entries.values() if "modal_price" in e]
|
| 69 |
+
if prices:
|
| 70 |
+
latest_prices[crop_name] = round(sum(prices) / len(prices), 2)
|
| 71 |
+
|
| 72 |
+
if latest_prices:
|
| 73 |
+
latest_ref.set({
|
| 74 |
+
"prices": latest_prices,
|
| 75 |
+
"timestamp": datetime.now().isoformat(),
|
| 76 |
+
"status": "computed",
|
| 77 |
+
"source": "seeded_data"
|
| 78 |
+
})
|
| 79 |
+
latest_data = {
|
| 80 |
+
"prices": latest_prices,
|
| 81 |
+
"timestamp": datetime.now().isoformat()
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
if crop:
|
| 85 |
+
price = latest_data.get("prices", {}).get(crop)
|
| 86 |
+
if not price:
|
| 87 |
+
available = list(latest_data.get("prices", {}).keys())
|
| 88 |
+
return {
|
| 89 |
+
"error": f"No price data for {crop}",
|
| 90 |
+
"available_crops": available,
|
| 91 |
+
"suggestion": f"Try one of: {', '.join(available[:5])}"
|
| 92 |
+
}
|
| 93 |
+
return {
|
| 94 |
+
"crop": crop,
|
| 95 |
+
"price": price,
|
| 96 |
+
"timestamp": latest_data.get("timestamp"),
|
| 97 |
+
"source": "firebase_seeded"
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
return latest_data
|
| 101 |
+
|
| 102 |
+
except Exception as e:
|
| 103 |
+
return {"error": f"Firebase error: {str(e)}"}
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def get_historical_prices_impl(crop: str, days: int = 30) -> dict[str, Any]:
|
| 107 |
+
"""Retrieve historical price data for a specific crop"""
|
| 108 |
+
try:
|
| 109 |
+
prices_ref = get_db_ref(f"market_data/prices/{crop}")
|
| 110 |
+
all_dates = prices_ref.get()
|
| 111 |
+
|
| 112 |
+
if not all_dates:
|
| 113 |
+
return {
|
| 114 |
+
"crop": crop,
|
| 115 |
+
"error": f"No historical data for {crop}. Database may not be seeded.",
|
| 116 |
+
"data_points": [],
|
| 117 |
+
"requires_seeding": True
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
cutoff_date = datetime.now() - timedelta(days=days)
|
| 121 |
+
price_history = []
|
| 122 |
+
|
| 123 |
+
for date_key, entries in all_dates.items():
|
| 124 |
+
try:
|
| 125 |
+
date_obj = datetime.strptime(date_key, "%Y-%m-%d")
|
| 126 |
+
|
| 127 |
+
if date_obj >= cutoff_date:
|
| 128 |
+
if isinstance(entries, dict):
|
| 129 |
+
prices = []
|
| 130 |
+
min_prices = []
|
| 131 |
+
max_prices = []
|
| 132 |
+
|
| 133 |
+
for entry in entries.values():
|
| 134 |
+
if isinstance(entry, dict) and "modal_price" in entry:
|
| 135 |
+
prices.append(entry["modal_price"])
|
| 136 |
+
min_prices.append(entry.get("min_price", entry["modal_price"]))
|
| 137 |
+
max_prices.append(entry.get("max_price", entry["modal_price"]))
|
| 138 |
+
|
| 139 |
+
if prices:
|
| 140 |
+
avg_price = sum(prices) / len(prices)
|
| 141 |
+
|
| 142 |
+
price_history.append({
|
| 143 |
+
"date": date_key,
|
| 144 |
+
"timestamp": date_obj.isoformat(),
|
| 145 |
+
"modal_price": round(avg_price, 2),
|
| 146 |
+
"min_price": round(min(min_prices), 2),
|
| 147 |
+
"max_price": round(max(max_prices), 2),
|
| 148 |
+
"entries_count": len(prices)
|
| 149 |
+
})
|
| 150 |
+
except ValueError:
|
| 151 |
+
continue
|
| 152 |
+
except Exception:
|
| 153 |
+
continue
|
| 154 |
+
|
| 155 |
+
price_history.sort(key=lambda x: x["date"])
|
| 156 |
+
|
| 157 |
+
if len(price_history) == 0:
|
| 158 |
+
return {
|
| 159 |
+
"crop": crop,
|
| 160 |
+
"error": "No data points in requested date range",
|
| 161 |
+
"data_points": [],
|
| 162 |
+
"suggestion": "Try seeding more data or increasing days parameter"
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
return {
|
| 166 |
+
"crop": crop,
|
| 167 |
+
"days_requested": days,
|
| 168 |
+
"data_points": price_history,
|
| 169 |
+
"total_records": len(price_history)
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
except Exception as e:
|
| 173 |
+
return {"crop": crop, "error": str(e), "data_points": []}
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def calculate_price_trend_impl(crop: str, days: int = 7) -> dict[str, Any]:
|
| 177 |
+
"""Analyze price trends and calculate statistics"""
|
| 178 |
+
try:
|
| 179 |
+
history = get_historical_prices_impl(crop, days)
|
| 180 |
+
|
| 181 |
+
if history.get("error") or not history.get("data_points"):
|
| 182 |
+
return {
|
| 183 |
+
"crop": crop,
|
| 184 |
+
"trend": "unavailable",
|
| 185 |
+
"error": history.get("error", "Insufficient data"),
|
| 186 |
+
"confidence": "none"
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
data_points = history["data_points"]
|
| 190 |
+
|
| 191 |
+
if len(data_points) < 2:
|
| 192 |
+
return {
|
| 193 |
+
"crop": crop,
|
| 194 |
+
"trend": "unavailable",
|
| 195 |
+
"error": "Need at least 2 data points for trend analysis",
|
| 196 |
+
"confidence": "none",
|
| 197 |
+
"data_points": len(data_points)
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
prices = [p["modal_price"] for p in data_points]
|
| 201 |
+
|
| 202 |
+
current_price = prices[-1]
|
| 203 |
+
starting_price = prices[0]
|
| 204 |
+
percent_change = ((current_price - starting_price) / starting_price) * 100
|
| 205 |
+
|
| 206 |
+
if len(prices) >= 3:
|
| 207 |
+
price_changes = []
|
| 208 |
+
for i in range(1, len(prices)):
|
| 209 |
+
change = abs(((prices[i] - prices[i-1]) / prices[i-1]) * 100)
|
| 210 |
+
price_changes.append(change)
|
| 211 |
+
avg_volatility = sum(price_changes) / len(price_changes)
|
| 212 |
+
volatility_level = "high" if avg_volatility > VOLATILITY_THRESHOLD else "moderate" if avg_volatility > 2 else "low"
|
| 213 |
+
else:
|
| 214 |
+
avg_volatility = abs(percent_change)
|
| 215 |
+
volatility_level = "low"
|
| 216 |
+
|
| 217 |
+
if percent_change > 2:
|
| 218 |
+
trend = "rising"
|
| 219 |
+
elif percent_change < -2:
|
| 220 |
+
trend = "falling"
|
| 221 |
+
else:
|
| 222 |
+
trend = "stable"
|
| 223 |
+
|
| 224 |
+
confidence = "high" if len(prices) >= 7 else "medium" if len(prices) >= 4 else "low"
|
| 225 |
+
|
| 226 |
+
avg_7day = sum(prices[-7:]) / min(7, len(prices))
|
| 227 |
+
avg_30day = sum(prices[-30:]) / min(30, len(prices)) if len(prices) > 7 else None
|
| 228 |
+
|
| 229 |
+
result = {
|
| 230 |
+
"crop": crop,
|
| 231 |
+
"trend": trend,
|
| 232 |
+
"percent_change": round(percent_change, 2),
|
| 233 |
+
"current_price": round(current_price, 2),
|
| 234 |
+
"starting_price": round(starting_price, 2),
|
| 235 |
+
"avg_price_7day": round(avg_7day, 2),
|
| 236 |
+
"avg_price_30day": round(avg_30day, 2) if avg_30day else None,
|
| 237 |
+
"volatility": volatility_level,
|
| 238 |
+
"avg_volatility": round(avg_volatility, 2),
|
| 239 |
+
"confidence": confidence,
|
| 240 |
+
"data_points": len(prices),
|
| 241 |
+
"analysis_period": f"{days} days",
|
| 242 |
+
"price_range": {
|
| 243 |
+
"min": round(min(prices), 2),
|
| 244 |
+
"max": round(max(prices), 2)
|
| 245 |
+
},
|
| 246 |
+
"recent_prices": [round(p, 2) for p in prices[-5:]]
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
return result
|
| 250 |
+
|
| 251 |
+
except Exception as e:
|
| 252 |
+
return {
|
| 253 |
+
"crop": crop,
|
| 254 |
+
"trend": "error",
|
| 255 |
+
"error": str(e),
|
| 256 |
+
"confidence": "none"
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def get_seasonal_context_impl(crop: str) -> dict[str, Any]:
|
| 261 |
+
"""Get seasonal planting and harvest information"""
|
| 262 |
+
try:
|
| 263 |
+
current_month = datetime.now().month
|
| 264 |
+
|
| 265 |
+
if crop not in PAKISTAN_CROP_SEASONS:
|
| 266 |
+
return {
|
| 267 |
+
"crop": crop,
|
| 268 |
+
"seasonal_advice": "No seasonal data available for this crop",
|
| 269 |
+
"in_planting_season": False,
|
| 270 |
+
"in_harvest_season": False
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
seasons = PAKISTAN_CROP_SEASONS[crop]
|
| 274 |
+
in_planting = current_month in seasons["plant"]
|
| 275 |
+
in_harvest = current_month in seasons["harvest"]
|
| 276 |
+
|
| 277 |
+
if in_planting:
|
| 278 |
+
advice = "PLANTING SEASON - High demand for inputs, optimal purchasing time"
|
| 279 |
+
elif in_harvest:
|
| 280 |
+
advice = "HARVEST SEASON - Prime selling period, market supply increases"
|
| 281 |
+
else:
|
| 282 |
+
advice = "Off-season period"
|
| 283 |
+
|
| 284 |
+
return {
|
| 285 |
+
"crop": crop,
|
| 286 |
+
"current_month": current_month,
|
| 287 |
+
"in_planting_season": in_planting,
|
| 288 |
+
"in_harvest_season": in_harvest,
|
| 289 |
+
"seasonal_advice": advice,
|
| 290 |
+
"plant_months": seasons["plant"],
|
| 291 |
+
"harvest_months": seasons["harvest"]
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
except Exception as e:
|
| 295 |
+
return {
|
| 296 |
+
"crop": crop,
|
| 297 |
+
"error": str(e),
|
| 298 |
+
"seasonal_advice": "Unable to determine seasonal context"
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
def get_market_statistics_impl(crop: str) -> dict[str, Any]:
|
| 303 |
+
"""Get comprehensive market statistics for a crop"""
|
| 304 |
+
try:
|
| 305 |
+
prices_ref = get_db_ref(f"market_data/prices/{crop}")
|
| 306 |
+
all_data = prices_ref.get() or {}
|
| 307 |
+
|
| 308 |
+
if not all_data:
|
| 309 |
+
return {"crop": crop, "error": "No data available"}
|
| 310 |
+
|
| 311 |
+
all_prices = []
|
| 312 |
+
provinces = set()
|
| 313 |
+
cities = set()
|
| 314 |
+
|
| 315 |
+
for date_key, entries in all_data.items():
|
| 316 |
+
if isinstance(entries, dict):
|
| 317 |
+
for entry in entries.values():
|
| 318 |
+
if "modal_price" in entry:
|
| 319 |
+
all_prices.append(entry["modal_price"])
|
| 320 |
+
if "province" in entry:
|
| 321 |
+
provinces.add(entry["province"])
|
| 322 |
+
if "city" in entry:
|
| 323 |
+
cities.add(entry["city"])
|
| 324 |
+
|
| 325 |
+
if not all_prices:
|
| 326 |
+
return {"crop": crop, "error": "No price data"}
|
| 327 |
+
|
| 328 |
+
return {
|
| 329 |
+
"crop": crop,
|
| 330 |
+
"total_entries": len(all_prices),
|
| 331 |
+
"provinces": list(provinces),
|
| 332 |
+
"cities": list(cities),
|
| 333 |
+
"price_statistics": {
|
| 334 |
+
"min": round(min(all_prices), 2),
|
| 335 |
+
"max": round(max(all_prices), 2),
|
| 336 |
+
"average": round(sum(all_prices) / len(all_prices), 2),
|
| 337 |
+
"median": round(statistics.median(all_prices), 2),
|
| 338 |
+
"std_deviation": round(statistics.stdev(all_prices), 2) if len(all_prices) > 1 else 0
|
| 339 |
+
}
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
except Exception as e:
|
| 343 |
+
return {"crop": crop, "error": str(e)}
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
# -------------------- TOOL WRAPPERS FOR AGENT --------------------
|
| 347 |
+
# These wrap the implementations for use by the Agent framework
|
| 348 |
+
|
| 349 |
+
@function_tool
|
| 350 |
+
def get_current_prices(crop: Optional[str] = None) -> dict[str, Any]:
|
| 351 |
+
"""Get current market prices from Firebase seeded data"""
|
| 352 |
+
return get_current_prices_impl(crop)
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
@function_tool
|
| 356 |
+
def get_historical_prices(crop: str, days: int = 30) -> dict[str, Any]:
|
| 357 |
+
"""Retrieve historical price data for a specific crop"""
|
| 358 |
+
return get_historical_prices_impl(crop, days)
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
@function_tool
|
| 362 |
+
def calculate_price_trend(crop: str, days: int = 7) -> dict[str, Any]:
|
| 363 |
+
"""Analyze price trends and calculate statistics"""
|
| 364 |
+
return calculate_price_trend_impl(crop, days)
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
@function_tool
|
| 368 |
+
def get_seasonal_context(crop: str) -> dict[str, Any]:
|
| 369 |
+
"""Get seasonal planting and harvest information"""
|
| 370 |
+
return get_seasonal_context_impl(crop)
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
@function_tool
|
| 374 |
+
def get_market_statistics(crop: str) -> dict[str, Any]:
|
| 375 |
+
"""Get comprehensive market statistics for a crop"""
|
| 376 |
+
return get_market_statistics_impl(crop)
|
| 377 |
+
|
| 378 |
+
|
| 379 |
+
# -------------------- MARKET AGENT --------------------
|
| 380 |
+
market_agent = Agent(
|
| 381 |
+
name="MarketAdvisor",
|
| 382 |
+
instructions="""You are an Expert Agricultural Market Intelligence Agent for Pakistani farmers using REAL SEEDED DATA from Firebase.
|
| 383 |
+
|
| 384 |
+
๐ฏ MANDATORY WORKFLOW:
|
| 385 |
+
|
| 386 |
+
1. ALWAYS call get_current_prices(crop) first
|
| 387 |
+
2. ALWAYS call calculate_price_trend(crop, 7) second
|
| 388 |
+
3. ALWAYS call get_seasonal_context(crop) third
|
| 389 |
+
4. Optionally call get_market_statistics(crop) for deeper insights
|
| 390 |
+
|
| 391 |
+
If ANY tool returns an error about missing data or "requires_seeding", immediately inform the user that the database needs to be seeded first and stop.
|
| 392 |
+
|
| 393 |
+
๐ RESPONSE FORMAT (use exactly this structure):
|
| 394 |
+
|
| 395 |
+
๐พ [CROP NAME] MARKET ADVISORY
|
| 396 |
+
|
| 397 |
+
**Action:** [SELL NOW / BUY INPUTS / HOLD / WAIT]
|
| 398 |
+
**Current Price:** โจ[X,XXX]/40kg
|
| 399 |
+
**Price Trend:** [rising/falling/stable] by [ยฑX.X]% over 7 days
|
| 400 |
+
**Confidence:** [HIGH/MEDIUM/LOW]
|
| 401 |
+
|
| 402 |
+
**Market Analysis:**
|
| 403 |
+
โข Current Price: โจ[X,XXX] (was โจ[Y,YYY] 7 days ago)
|
| 404 |
+
โข Price Change: [+/-]X.X% over analysis period
|
| 405 |
+
โข Volatility: [low/moderate/high] market conditions
|
| 406 |
+
โข Seasonal Context: [Current month situation]
|
| 407 |
+
โข Data Quality: [N] price records analyzed
|
| 408 |
+
|
| 409 |
+
**Recommendation:**
|
| 410 |
+
[Specific actionable advice based on actual data]
|
| 411 |
+
|
| 412 |
+
**Risk Factors:**
|
| 413 |
+
โข [Specific risk based on trend data]
|
| 414 |
+
โข [Specific risk based on seasonality]
|
| 415 |
+
โข [Specific risk based on volatility]
|
| 416 |
+
|
| 417 |
+
๐ก DECISION LOGIC:
|
| 418 |
+
- SELL NOW: Harvest season AND (rising >5% OR spike >10%)
|
| 419 |
+
- BUY INPUTS: Planting season AND (falling >5% OR drop >10%)
|
| 420 |
+
- HOLD: Stable prices (<2% change) AND no seasonal pressure
|
| 421 |
+
- WAIT: High volatility (>5%) OR conflicting signals
|
| 422 |
+
|
| 423 |
+
๐ DATA RULES:
|
| 424 |
+
1. Use ONLY actual numbers from tool responses
|
| 425 |
+
2. ALWAYS mention percent_change value from calculate_price_trend()
|
| 426 |
+
3. ALWAYS mention data_points count
|
| 427 |
+
4. ALWAYS reference volatility level
|
| 428 |
+
5. Base ALL recommendations on Firebase data
|
| 429 |
+
|
| 430 |
+
๐ซ NEVER:
|
| 431 |
+
- Provide advice without calling calculate_price_trend()
|
| 432 |
+
- Use vague phrases without specific numbers
|
| 433 |
+
- Ignore errors from tools
|
| 434 |
+
- Make up data if tools fail""",
|
| 435 |
+
model=model,
|
| 436 |
+
tools=[
|
| 437 |
+
get_current_prices,
|
| 438 |
+
get_historical_prices,
|
| 439 |
+
calculate_price_trend,
|
| 440 |
+
get_seasonal_context,
|
| 441 |
+
get_market_statistics
|
| 442 |
+
],
|
| 443 |
+
)
|
| 444 |
+
|
| 445 |
+
# -------------------- RUNNER --------------------
|
| 446 |
+
runner = Runner()
|
| 447 |
+
|
| 448 |
+
|
| 449 |
+
async def get_market_advice(crop: str, farmer_data: Optional[Dict[str, Any]] = None) -> str:
|
| 450 |
+
"""Get comprehensive market advice for a specific crop"""
|
| 451 |
+
|
| 452 |
+
prompt = f"""Provide comprehensive market advice for {crop.upper()} using the seeded Firebase data.
|
| 453 |
+
|
| 454 |
+
CRITICAL: You MUST call these tools in order:
|
| 455 |
+
1. get_current_prices("{crop}")
|
| 456 |
+
2. calculate_price_trend("{crop}", 7)
|
| 457 |
+
3. get_seasonal_context("{crop}")
|
| 458 |
+
|
| 459 |
+
If any tool returns an error with "requires_seeding" or "No data found", stop immediately and tell the user to seed the database first.
|
| 460 |
+
|
| 461 |
+
Then provide your complete advisory using the exact format specified in your instructions."""
|
| 462 |
+
|
| 463 |
+
if farmer_data:
|
| 464 |
+
context = []
|
| 465 |
+
if farmer_data.get('budget'):
|
| 466 |
+
context.append(f"budget: PKR {farmer_data['budget']:,}")
|
| 467 |
+
if farmer_data.get('land_size'):
|
| 468 |
+
context.append(f"land: {farmer_data['land_size']} acres")
|
| 469 |
+
if farmer_data.get('risk_tolerance'):
|
| 470 |
+
context.append(f"risk tolerance: {farmer_data['risk_tolerance']}")
|
| 471 |
+
|
| 472 |
+
if context:
|
| 473 |
+
prompt += f"\n\nFarmer context: {', '.join(context)}"
|
| 474 |
+
|
| 475 |
+
print(f"\n{'='*60}")
|
| 476 |
+
print(f"๐ค MARKET AGENT REQUEST: {crop.upper()}")
|
| 477 |
+
print(f"{'='*60}")
|
| 478 |
+
|
| 479 |
+
try:
|
| 480 |
+
result = await runner.run(market_agent, prompt)
|
| 481 |
+
|
| 482 |
+
response = None
|
| 483 |
+
if hasattr(result, "final_output") and result.final_output:
|
| 484 |
+
response = result.final_output
|
| 485 |
+
elif hasattr(result, "output") and result.output:
|
| 486 |
+
response = result.output
|
| 487 |
+
elif hasattr(result, "messages") and result.messages:
|
| 488 |
+
for msg in reversed(result.messages):
|
| 489 |
+
if hasattr(msg, "role") and msg.role == "assistant":
|
| 490 |
+
if hasattr(msg, "content") and msg.content:
|
| 491 |
+
response = str(msg.content)
|
| 492 |
+
break
|
| 493 |
+
|
| 494 |
+
if not response:
|
| 495 |
+
return f"""๐พ {crop.upper()} MARKET ADVISORY
|
| 496 |
+
|
| 497 |
+
**Status:** ERROR
|
| 498 |
+
|
| 499 |
+
Unable to generate advice. The agent did not return a valid response.
|
| 500 |
+
|
| 501 |
+
**Action Required:**
|
| 502 |
+
1. Ensure database is seeded: POST /admin/seed-market-data
|
| 503 |
+
2. Check Firebase connection
|
| 504 |
+
3. Verify agent configuration
|
| 505 |
+
|
| 506 |
+
Please seed the database first and try again."""
|
| 507 |
+
|
| 508 |
+
print(f"โ
Market advice generated successfully")
|
| 509 |
+
return response
|
| 510 |
+
|
| 511 |
+
except Exception as e:
|
| 512 |
+
print(f"โ Agent error: {e}")
|
| 513 |
+
import traceback
|
| 514 |
+
traceback.print_exc()
|
| 515 |
+
return f"""๐พ {crop.upper()} MARKET ADVISORY
|
| 516 |
+
|
| 517 |
+
**Status:** ERROR
|
| 518 |
+
|
| 519 |
+
Failed to generate market advice: {str(e)}
|
| 520 |
+
|
| 521 |
+
**Action Required:**
|
| 522 |
+
Ensure the database has been seeded with market data by calling:
|
| 523 |
+
POST /admin/seed-market-data
|
| 524 |
+
|
| 525 |
+
Then try again."""
|
ageents/predictionAgent.py
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FIXED Prediction Workflow Agent - Synthesizes Weather and Soil data
|
| 3 |
+
Location: ageents/predictionAgent.py
|
| 4 |
+
|
| 5 |
+
FIX: Added AgentOutputSchema wrapper to handle strict JSON schema validation
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import json
|
| 10 |
+
import asyncio
|
| 11 |
+
from dotenv import load_dotenv, find_dotenv
|
| 12 |
+
from pydantic import BaseModel
|
| 13 |
+
from typing import Dict, Any, List, Optional
|
| 14 |
+
from datetime import datetime
|
| 15 |
+
|
| 16 |
+
from agents import (
|
| 17 |
+
Agent,
|
| 18 |
+
Runner,
|
| 19 |
+
AsyncOpenAI,
|
| 20 |
+
OpenAIChatCompletionsModel,
|
| 21 |
+
set_tracing_disabled,
|
| 22 |
+
SQLiteSession,
|
| 23 |
+
AgentOutputSchema, # โ ADDED THIS
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
# Import the specialized agents
|
| 27 |
+
from ageents.weatherAgent import run_weather_agent
|
| 28 |
+
from ageents.soilAgent import run_soil_agent
|
| 29 |
+
|
| 30 |
+
# ---------------- Setup ----------------
|
| 31 |
+
load_dotenv(find_dotenv())
|
| 32 |
+
set_tracing_disabled(True)
|
| 33 |
+
|
| 34 |
+
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
| 35 |
+
|
| 36 |
+
if not GEMINI_API_KEY:
|
| 37 |
+
raise RuntimeError("Missing Gemini API key in .env -> GEMINI_API_KEY")
|
| 38 |
+
|
| 39 |
+
client_provider = AsyncOpenAI(
|
| 40 |
+
api_key=GEMINI_API_KEY,
|
| 41 |
+
base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
|
| 42 |
+
)
|
| 43 |
+
Model = OpenAIChatCompletionsModel(
|
| 44 |
+
model="gemini-2.0-flash",
|
| 45 |
+
openai_client=client_provider,
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
runner = Runner()
|
| 49 |
+
|
| 50 |
+
# ---------------- Schemas ----------------
|
| 51 |
+
class PredictionInput(BaseModel):
|
| 52 |
+
location: str
|
| 53 |
+
crop: str
|
| 54 |
+
city: Optional[str] = None # For weather if different from location
|
| 55 |
+
|
| 56 |
+
class PredictionOutput(BaseModel):
|
| 57 |
+
location: str
|
| 58 |
+
crop: str
|
| 59 |
+
timestamp: str
|
| 60 |
+
|
| 61 |
+
# Weather insights
|
| 62 |
+
weather_summary: Dict[str, Any]
|
| 63 |
+
weather_risks: List[str]
|
| 64 |
+
weather_opportunities: List[str]
|
| 65 |
+
|
| 66 |
+
# Soil insights
|
| 67 |
+
soil_summary: Dict[str, Any]
|
| 68 |
+
soil_risks: List[str]
|
| 69 |
+
soil_opportunities: List[str]
|
| 70 |
+
|
| 71 |
+
# Integrated predictions
|
| 72 |
+
overall_prediction: str
|
| 73 |
+
risk_level: str # low, moderate, high, critical
|
| 74 |
+
success_probability: float
|
| 75 |
+
yield_forecast: str
|
| 76 |
+
|
| 77 |
+
# Actionable recommendations
|
| 78 |
+
immediate_actions: List[str]
|
| 79 |
+
short_term_actions: List[str] # 1-2 weeks
|
| 80 |
+
long_term_actions: List[str] # 1+ months
|
| 81 |
+
|
| 82 |
+
# Resource optimization
|
| 83 |
+
irrigation_schedule: Dict[str, Any]
|
| 84 |
+
fertilizer_schedule: Dict[str, Any]
|
| 85 |
+
pest_disease_risk: str
|
| 86 |
+
|
| 87 |
+
# Economic insights
|
| 88 |
+
optimal_harvest_window: str
|
| 89 |
+
market_timing_advice: str
|
| 90 |
+
|
| 91 |
+
# Summary
|
| 92 |
+
executive_summary: str
|
| 93 |
+
|
| 94 |
+
# ---------------- Prediction Workflow Agent ----------------
|
| 95 |
+
# โ CHANGED: Wrapped output_type with AgentOutputSchema
|
| 96 |
+
prediction_agent = Agent(
|
| 97 |
+
name="AgriPredictionWorkflowAgent",
|
| 98 |
+
model=Model,
|
| 99 |
+
instructions="""
|
| 100 |
+
You are the AgriTech Prediction Workflow Agent for the Farmer-Centric Multi-Agent AgriTech System (AgriLedger+).
|
| 101 |
+
|
| 102 |
+
Your role is to synthesize inputs from Weather and Soil agents to provide comprehensive, actionable farm predictions.
|
| 103 |
+
|
| 104 |
+
Input: JSON containing:
|
| 105 |
+
- weather_data: complete weather analysis with trends, precipitation, recommendations
|
| 106 |
+
- soil_data: complete soil analysis with health score, nutrients, recommendations
|
| 107 |
+
- crop: target crop
|
| 108 |
+
- location: farm location
|
| 109 |
+
|
| 110 |
+
Your task:
|
| 111 |
+
1. Analyze weather-soil interactions and their combined impact on the crop
|
| 112 |
+
2. Identify risks (e.g., heavy rain + low drainage soil = waterlogging risk)
|
| 113 |
+
3. Identify opportunities (e.g., warming trend + high nitrogen = optimal growth)
|
| 114 |
+
4. Provide risk level assessment: low/moderate/high/critical
|
| 115 |
+
5. Estimate success probability (0-100%)
|
| 116 |
+
6. Generate time-based action plans (immediate/short-term/long-term)
|
| 117 |
+
7. Optimize resource usage (irrigation, fertilizer) based on both inputs
|
| 118 |
+
8. Predict pest/disease risks based on weather and soil conditions
|
| 119 |
+
9. Suggest optimal harvest timing
|
| 120 |
+
10. Provide executive summary for decision-making
|
| 121 |
+
|
| 122 |
+
Output: Strict JSON matching the PredictionOutput schema
|
| 123 |
+
|
| 124 |
+
Constraints:
|
| 125 |
+
- Cross-reference weather and soil data for conflicts/synergies
|
| 126 |
+
- Prioritize immediate risks (next 48 hours)
|
| 127 |
+
- Provide specific, measurable recommendations
|
| 128 |
+
- Include confidence levels where applicable
|
| 129 |
+
- No generic advice - tailor everything to the specific crop, weather, and soil conditions
|
| 130 |
+
- Return strict JSON only
|
| 131 |
+
|
| 132 |
+
Example risk identification:
|
| 133 |
+
- High precipitation + low soil drainage = "waterlogging risk: immediate drainage required"
|
| 134 |
+
- Warming trend + nitrogen deficiency = "growth limitation: apply nitrogen fertilizer within 3 days"
|
| 135 |
+
- Low moisture + high temperature = "drought stress: increase irrigation by 30%"
|
| 136 |
+
|
| 137 |
+
Example opportunities:
|
| 138 |
+
- Optimal temperature + balanced nutrients = "peak growth phase: maintain current practices"
|
| 139 |
+
- Moderate rainfall + good soil health = "ideal conditions: consider early harvest"
|
| 140 |
+
""",
|
| 141 |
+
output_type=AgentOutputSchema(PredictionOutput, strict_json_schema=False), # โ FIX HERE
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
# ---------------- Helper functions ----------------
|
| 145 |
+
async def collect_agent_data(location: str, crop: str, city: Optional[str] = None):
|
| 146 |
+
"""Collect data from weather and soil agents"""
|
| 147 |
+
|
| 148 |
+
# Use city for weather if provided, otherwise use location
|
| 149 |
+
weather_city = city or location
|
| 150 |
+
|
| 151 |
+
print(f"๐ค๏ธ Fetching weather data for {weather_city}...")
|
| 152 |
+
weather_data = await run_weather_agent(weather_city)
|
| 153 |
+
|
| 154 |
+
print(f"๐ฑ Fetching soil data for {location} (crop: {crop})...")
|
| 155 |
+
soil_data = await run_soil_agent(location, crop)
|
| 156 |
+
|
| 157 |
+
return {
|
| 158 |
+
"weather": weather_data,
|
| 159 |
+
"soil": soil_data
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
def calculate_risk_level(weather_data: Dict, soil_data: Dict) -> str:
|
| 163 |
+
"""Calculate overall risk level based on weather and soil conditions"""
|
| 164 |
+
risk_score = 0
|
| 165 |
+
|
| 166 |
+
# Weather risks
|
| 167 |
+
precip_chance = weather_data.get("precipitation_chance", 0)
|
| 168 |
+
if precip_chance > 70:
|
| 169 |
+
risk_score += 3
|
| 170 |
+
elif precip_chance > 40:
|
| 171 |
+
risk_score += 2
|
| 172 |
+
|
| 173 |
+
trend = weather_data.get("trend", "stable")
|
| 174 |
+
if trend in ["warming", "cooling"]:
|
| 175 |
+
risk_score += 1
|
| 176 |
+
|
| 177 |
+
# Soil risks
|
| 178 |
+
health_score = soil_data.get("soil_health_score", 100)
|
| 179 |
+
if health_score < 50:
|
| 180 |
+
risk_score += 3
|
| 181 |
+
elif health_score < 70:
|
| 182 |
+
risk_score += 2
|
| 183 |
+
elif health_score < 85:
|
| 184 |
+
risk_score += 1
|
| 185 |
+
|
| 186 |
+
warnings = len(soil_data.get("warnings", []))
|
| 187 |
+
risk_score += warnings
|
| 188 |
+
|
| 189 |
+
# Classify risk
|
| 190 |
+
if risk_score >= 6:
|
| 191 |
+
return "critical"
|
| 192 |
+
elif risk_score >= 4:
|
| 193 |
+
return "high"
|
| 194 |
+
elif risk_score >= 2:
|
| 195 |
+
return "moderate"
|
| 196 |
+
else:
|
| 197 |
+
return "low"
|
| 198 |
+
|
| 199 |
+
def estimate_success_probability(risk_level: str, weather_data: Dict, soil_data: Dict) -> float:
|
| 200 |
+
"""Estimate crop success probability based on conditions"""
|
| 201 |
+
base_probability = {
|
| 202 |
+
"low": 85.0,
|
| 203 |
+
"moderate": 70.0,
|
| 204 |
+
"high": 55.0,
|
| 205 |
+
"critical": 35.0
|
| 206 |
+
}.get(risk_level, 70.0)
|
| 207 |
+
|
| 208 |
+
# Adjust based on soil health
|
| 209 |
+
soil_health = soil_data.get("soil_health_score", 70)
|
| 210 |
+
soil_factor = (soil_health - 70) * 0.3
|
| 211 |
+
|
| 212 |
+
# Adjust based on weather favorability
|
| 213 |
+
precip_chance = weather_data.get("precipitation_chance", 0)
|
| 214 |
+
if 20 <= precip_chance <= 40:
|
| 215 |
+
weather_factor = 5.0 # Ideal precipitation
|
| 216 |
+
elif precip_chance < 10:
|
| 217 |
+
weather_factor = -10.0 # Too dry
|
| 218 |
+
elif precip_chance > 70:
|
| 219 |
+
weather_factor = -10.0 # Too wet
|
| 220 |
+
else:
|
| 221 |
+
weather_factor = 0.0
|
| 222 |
+
|
| 223 |
+
final_probability = base_probability + soil_factor + weather_factor
|
| 224 |
+
return round(max(10.0, min(95.0, final_probability)), 2)
|
| 225 |
+
|
| 226 |
+
def generate_irrigation_schedule(weather_data: Dict, soil_data: Dict) -> Dict[str, Any]:
|
| 227 |
+
"""Generate irrigation schedule based on weather and soil"""
|
| 228 |
+
|
| 229 |
+
moisture = soil_data.get("soil_data", {}).get("moisture_percent", 20)
|
| 230 |
+
precip_chance = weather_data.get("precipitation_chance", 0)
|
| 231 |
+
trend = weather_data.get("trend", "stable")
|
| 232 |
+
|
| 233 |
+
# Base frequency
|
| 234 |
+
if moisture < 15:
|
| 235 |
+
frequency = "daily"
|
| 236 |
+
amount = "heavy"
|
| 237 |
+
elif moisture < 20:
|
| 238 |
+
frequency = "every 2 days"
|
| 239 |
+
amount = "moderate"
|
| 240 |
+
else:
|
| 241 |
+
frequency = "every 3 days"
|
| 242 |
+
amount = "light"
|
| 243 |
+
|
| 244 |
+
# Adjust for precipitation
|
| 245 |
+
if precip_chance > 50:
|
| 246 |
+
frequency = "pause for 3 days"
|
| 247 |
+
amount = "none"
|
| 248 |
+
elif precip_chance > 30:
|
| 249 |
+
frequency = f"{frequency} (reduce if rain occurs)"
|
| 250 |
+
amount = "light"
|
| 251 |
+
|
| 252 |
+
# Adjust for temperature trend
|
| 253 |
+
if trend == "warming":
|
| 254 |
+
amount = "increase by 20%"
|
| 255 |
+
elif trend == "cooling":
|
| 256 |
+
amount = "reduce by 10%"
|
| 257 |
+
|
| 258 |
+
return {
|
| 259 |
+
"frequency": frequency,
|
| 260 |
+
"amount": amount,
|
| 261 |
+
"method": "drip irrigation recommended",
|
| 262 |
+
"timing": "early morning (6-8 AM)",
|
| 263 |
+
"notes": f"Current moisture: {moisture}%, precipitation chance: {precip_chance}%"
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
def predict_pest_disease_risk(weather_data: Dict, soil_data: Dict, crop: str) -> str:
|
| 267 |
+
"""Predict pest and disease risk based on conditions"""
|
| 268 |
+
|
| 269 |
+
precip_chance = weather_data.get("precipitation_chance", 0)
|
| 270 |
+
moisture = soil_data.get("soil_data", {}).get("moisture_percent", 20)
|
| 271 |
+
health_score = soil_data.get("soil_health_score", 70)
|
| 272 |
+
|
| 273 |
+
risk_score = 0
|
| 274 |
+
|
| 275 |
+
# High humidity increases fungal disease risk
|
| 276 |
+
if precip_chance > 60 and moisture > 25:
|
| 277 |
+
risk_score += 3
|
| 278 |
+
elif precip_chance > 40 or moisture > 25:
|
| 279 |
+
risk_score += 2
|
| 280 |
+
|
| 281 |
+
# Poor soil health = more susceptible
|
| 282 |
+
if health_score < 60:
|
| 283 |
+
risk_score += 2
|
| 284 |
+
elif health_score < 75:
|
| 285 |
+
risk_score += 1
|
| 286 |
+
|
| 287 |
+
# Crop-specific risks
|
| 288 |
+
high_risk_crops = ["tomato", "potato", "rice"]
|
| 289 |
+
if crop.lower() in high_risk_crops:
|
| 290 |
+
risk_score += 1
|
| 291 |
+
|
| 292 |
+
if risk_score >= 5:
|
| 293 |
+
return "high - immediate preventive measures required"
|
| 294 |
+
elif risk_score >= 3:
|
| 295 |
+
return "moderate - monitor closely, prepare treatments"
|
| 296 |
+
else:
|
| 297 |
+
return "low - maintain regular monitoring"
|
| 298 |
+
|
| 299 |
+
def suggest_harvest_window(weather_data: Dict, soil_data: Dict, crop: str) -> str:
|
| 300 |
+
"""Suggest optimal harvest window"""
|
| 301 |
+
|
| 302 |
+
precip_chance = weather_data.get("precipitation_chance", 0)
|
| 303 |
+
trend = weather_data.get("trend", "stable")
|
| 304 |
+
|
| 305 |
+
# Base harvest timing by crop
|
| 306 |
+
crop_maturity = {
|
| 307 |
+
"wheat": "90-120 days",
|
| 308 |
+
"rice": "120-150 days",
|
| 309 |
+
"maize": "70-90 days",
|
| 310 |
+
"cotton": "150-180 days",
|
| 311 |
+
"potato": "70-90 days",
|
| 312 |
+
"tomato": "60-85 days"
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
base_window = crop_maturity.get(crop.lower(), "monitor crop maturity")
|
| 316 |
+
|
| 317 |
+
# Adjust for weather
|
| 318 |
+
if precip_chance > 60:
|
| 319 |
+
advice = f"{base_window} - consider early harvest before heavy rains"
|
| 320 |
+
elif trend == "warming":
|
| 321 |
+
advice = f"{base_window} - warming trend may accelerate maturity"
|
| 322 |
+
elif trend == "cooling":
|
| 323 |
+
advice = f"{base_window} - cooling may delay maturity slightly"
|
| 324 |
+
else:
|
| 325 |
+
advice = f"{base_window} - conditions favorable for normal schedule"
|
| 326 |
+
|
| 327 |
+
return advice
|
| 328 |
+
|
| 329 |
+
# ---------------- Main Workflow ----------------
|
| 330 |
+
async def run_prediction_workflow(location: str, crop: str, city: Optional[str] = None):
|
| 331 |
+
"""
|
| 332 |
+
Run the complete prediction workflow:
|
| 333 |
+
1. Collect weather and soil data
|
| 334 |
+
2. Analyze combined insights
|
| 335 |
+
3. Generate comprehensive predictions
|
| 336 |
+
"""
|
| 337 |
+
|
| 338 |
+
print("\n" + "="*70)
|
| 339 |
+
print("๐ AGRI PREDICTION WORKFLOW")
|
| 340 |
+
print("="*70)
|
| 341 |
+
print(f"๐ Location: {location}")
|
| 342 |
+
print(f"๐พ Crop: {crop}")
|
| 343 |
+
print(f"๐ค๏ธ Weather City: {city or location}")
|
| 344 |
+
print(f"โฐ Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
| 345 |
+
print("="*70 + "\n")
|
| 346 |
+
|
| 347 |
+
# Step 1: Collect data from specialized agents
|
| 348 |
+
try:
|
| 349 |
+
agent_data = await collect_agent_data(location, crop, city)
|
| 350 |
+
weather_data = agent_data["weather"]
|
| 351 |
+
soil_data = agent_data["soil"]
|
| 352 |
+
except Exception as e:
|
| 353 |
+
print(f"โ Data collection failed: {e}")
|
| 354 |
+
return {"error": f"Failed to collect agent data: {str(e)}"}
|
| 355 |
+
|
| 356 |
+
# Check for errors in individual agents
|
| 357 |
+
if "error" in weather_data:
|
| 358 |
+
return {"error": f"Weather agent error: {weather_data['error']}"}
|
| 359 |
+
if "error" in soil_data:
|
| 360 |
+
return {"error": f"Soil agent error: {soil_data['error']}"}
|
| 361 |
+
|
| 362 |
+
# Step 2: Calculate derived metrics
|
| 363 |
+
risk_level = calculate_risk_level(weather_data, soil_data)
|
| 364 |
+
success_probability = estimate_success_probability(risk_level, weather_data, soil_data)
|
| 365 |
+
irrigation_schedule = generate_irrigation_schedule(weather_data, soil_data)
|
| 366 |
+
pest_risk = predict_pest_disease_risk(weather_data, soil_data, crop)
|
| 367 |
+
harvest_window = suggest_harvest_window(weather_data, soil_data, crop)
|
| 368 |
+
|
| 369 |
+
# Step 3: Prepare message for prediction agent
|
| 370 |
+
message = json.dumps({
|
| 371 |
+
"location": location,
|
| 372 |
+
"crop": crop,
|
| 373 |
+
"weather_data": weather_data,
|
| 374 |
+
"soil_data": soil_data,
|
| 375 |
+
"calculated_risk_level": risk_level,
|
| 376 |
+
"calculated_success_probability": success_probability,
|
| 377 |
+
"irrigation_schedule": irrigation_schedule,
|
| 378 |
+
"pest_risk": pest_risk,
|
| 379 |
+
"harvest_window": harvest_window,
|
| 380 |
+
"timestamp": datetime.now().isoformat()
|
| 381 |
+
}, ensure_ascii=False)
|
| 382 |
+
|
| 383 |
+
# Step 4: Run prediction agent
|
| 384 |
+
print("๐ค Running prediction synthesis agent...\n")
|
| 385 |
+
try:
|
| 386 |
+
resp = await runner.run(
|
| 387 |
+
prediction_agent,
|
| 388 |
+
message,
|
| 389 |
+
session=SQLiteSession("trace.db")
|
| 390 |
+
)
|
| 391 |
+
if hasattr(resp, "output"):
|
| 392 |
+
prediction_output = resp.output.model_dump()
|
| 393 |
+
elif hasattr(resp, "final_output"):
|
| 394 |
+
prediction_output = resp.final_output.model_dump()
|
| 395 |
+
else:
|
| 396 |
+
prediction_output = {}
|
| 397 |
+
except Exception as e:
|
| 398 |
+
print(f"โ Prediction agent failed: {e}")
|
| 399 |
+
prediction_output = {}
|
| 400 |
+
|
| 401 |
+
# Step 5: Fallback logic if agent fails
|
| 402 |
+
if not prediction_output:
|
| 403 |
+
print("โ ๏ธ Agent failed, generating fallback prediction...\n")
|
| 404 |
+
|
| 405 |
+
# Extract key insights
|
| 406 |
+
weather_summary = {
|
| 407 |
+
"trend": weather_data.get("trend", "unknown"),
|
| 408 |
+
"precipitation_chance": weather_data.get("precipitation_chance", 0),
|
| 409 |
+
"recommendations": weather_data.get("recommendations", [])
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
soil_summary = {
|
| 413 |
+
"health_score": soil_data.get("soil_health_score", 0),
|
| 414 |
+
"nutrient_status": soil_data.get("nutrient_status", {}),
|
| 415 |
+
"deficiencies": soil_data.get("deficiencies", [])
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
# Identify risks
|
| 419 |
+
weather_risks = []
|
| 420 |
+
if weather_data.get("precipitation_chance", 0) > 60:
|
| 421 |
+
weather_risks.append("high precipitation risk - waterlogging possible")
|
| 422 |
+
if weather_data.get("trend") == "warming":
|
| 423 |
+
weather_risks.append("warming trend - increased water demand")
|
| 424 |
+
|
| 425 |
+
soil_risks = soil_data.get("warnings", [])
|
| 426 |
+
|
| 427 |
+
# Identify opportunities
|
| 428 |
+
weather_opportunities = []
|
| 429 |
+
soil_opportunities = []
|
| 430 |
+
|
| 431 |
+
if 20 <= weather_data.get("precipitation_chance", 0) <= 40:
|
| 432 |
+
weather_opportunities.append("ideal rainfall conditions")
|
| 433 |
+
if soil_data.get("soil_health_score", 0) > 75:
|
| 434 |
+
soil_opportunities.append("good soil health - optimal for growth")
|
| 435 |
+
|
| 436 |
+
# Generate recommendations
|
| 437 |
+
immediate_actions = []
|
| 438 |
+
immediate_actions.extend(weather_data.get("recommendations", [])[:2])
|
| 439 |
+
immediate_actions.extend(soil_data.get("recommendations", [])[:2])
|
| 440 |
+
|
| 441 |
+
# Remove duplicates
|
| 442 |
+
immediate_actions = list(dict.fromkeys(immediate_actions))
|
| 443 |
+
|
| 444 |
+
short_term_actions = [
|
| 445 |
+
"monitor weather changes daily",
|
| 446 |
+
"track soil moisture levels",
|
| 447 |
+
"inspect crop for pest/disease signs"
|
| 448 |
+
]
|
| 449 |
+
|
| 450 |
+
long_term_actions = [
|
| 451 |
+
"improve soil organic matter content",
|
| 452 |
+
"implement precision farming techniques",
|
| 453 |
+
"plan crop rotation for next season"
|
| 454 |
+
]
|
| 455 |
+
|
| 456 |
+
# Generate yield forecast
|
| 457 |
+
if success_probability > 75:
|
| 458 |
+
yield_forecast = "good to excellent yield expected"
|
| 459 |
+
elif success_probability > 60:
|
| 460 |
+
yield_forecast = "moderate to good yield expected"
|
| 461 |
+
elif success_probability > 45:
|
| 462 |
+
yield_forecast = "fair yield expected with proper management"
|
| 463 |
+
else:
|
| 464 |
+
yield_forecast = "below average yield - intervention required"
|
| 465 |
+
|
| 466 |
+
prediction_output = {
|
| 467 |
+
"location": location,
|
| 468 |
+
"crop": crop,
|
| 469 |
+
"timestamp": datetime.now().isoformat(),
|
| 470 |
+
|
| 471 |
+
"weather_summary": weather_summary,
|
| 472 |
+
"weather_risks": weather_risks or ["minimal weather risks"],
|
| 473 |
+
"weather_opportunities": weather_opportunities or ["stable conditions"],
|
| 474 |
+
|
| 475 |
+
"soil_summary": soil_summary,
|
| 476 |
+
"soil_risks": soil_risks or ["no critical soil issues"],
|
| 477 |
+
"soil_opportunities": soil_opportunities or ["maintain current practices"],
|
| 478 |
+
|
| 479 |
+
"overall_prediction": f"{crop} cultivation at {location} - {risk_level} risk level",
|
| 480 |
+
"risk_level": risk_level,
|
| 481 |
+
"success_probability": success_probability,
|
| 482 |
+
"yield_forecast": yield_forecast,
|
| 483 |
+
|
| 484 |
+
"immediate_actions": immediate_actions,
|
| 485 |
+
"short_term_actions": short_term_actions,
|
| 486 |
+
"long_term_actions": long_term_actions,
|
| 487 |
+
|
| 488 |
+
"irrigation_schedule": irrigation_schedule,
|
| 489 |
+
"fertilizer_schedule": soil_data.get("fertilizer_plan", {}),
|
| 490 |
+
"pest_disease_risk": pest_risk,
|
| 491 |
+
|
| 492 |
+
"optimal_harvest_window": harvest_window,
|
| 493 |
+
"market_timing_advice": "consult market agent for current price trends",
|
| 494 |
+
|
| 495 |
+
"executive_summary": (
|
| 496 |
+
f"Risk Level: {risk_level.upper()}. "
|
| 497 |
+
f"Success Probability: {success_probability}%. "
|
| 498 |
+
f"Weather: {weather_data.get('trend', 'stable')} trend, "
|
| 499 |
+
f"{weather_data.get('precipitation_chance', 0)}% rain chance. "
|
| 500 |
+
f"Soil Health: {soil_data.get('soil_health_score', 0)}/100. "
|
| 501 |
+
f"Key actions: {', '.join(immediate_actions[:3])}."
|
| 502 |
+
)
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
# Step 6: Display results
|
| 506 |
+
print("="*70)
|
| 507 |
+
print("๐ PREDICTION RESULTS")
|
| 508 |
+
print("="*70)
|
| 509 |
+
print(f"โ ๏ธ Risk Level: {prediction_output['risk_level'].upper()}")
|
| 510 |
+
print(f"๐ Success Probability: {prediction_output['success_probability']}%")
|
| 511 |
+
print(f"๐ฏ Yield Forecast: {prediction_output.get('yield_forecast', 'N/A')}")
|
| 512 |
+
print(f"๐ Pest/Disease Risk: {prediction_output.get('pest_disease_risk', 'N/A')}")
|
| 513 |
+
print("\n๐ด IMMEDIATE ACTIONS (Next 48 hours):")
|
| 514 |
+
for action in prediction_output.get('immediate_actions', [])[:5]:
|
| 515 |
+
print(f" โข {action}")
|
| 516 |
+
print("\n๐ Executive Summary:")
|
| 517 |
+
print(f" {prediction_output.get('executive_summary', 'N/A')}")
|
| 518 |
+
print("="*70 + "\n")
|
| 519 |
+
|
| 520 |
+
return prediction_output
|
| 521 |
+
|
| 522 |
+
# ---------------- Run standalone ----------------
|
| 523 |
+
if __name__ == "__main__":
|
| 524 |
+
print("๐พ AgriLedger+ Prediction Workflow Agent")
|
| 525 |
+
print("=" * 70)
|
| 526 |
+
|
| 527 |
+
location_input = input("Enter farm location: ")
|
| 528 |
+
crop_input = input("Enter crop name: ")
|
| 529 |
+
city_input = input("Enter city for weather (press Enter to use location): ").strip() or None
|
| 530 |
+
|
| 531 |
+
result = asyncio.run(run_prediction_workflow(location_input, crop_input, city_input))
|
| 532 |
+
|
| 533 |
+
print("\n๐ Full JSON Output:")
|
| 534 |
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
ageents/soilAgent.py
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import asyncio
|
| 4 |
+
from dotenv import load_dotenv, find_dotenv
|
| 5 |
+
from pydantic import BaseModel
|
| 6 |
+
from typing import Dict, Any, List, Optional
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
|
| 9 |
+
from agents import (
|
| 10 |
+
Agent,
|
| 11 |
+
Runner,
|
| 12 |
+
AsyncOpenAI,
|
| 13 |
+
OpenAIChatCompletionsModel,
|
| 14 |
+
set_tracing_disabled,
|
| 15 |
+
SQLiteSession,
|
| 16 |
+
)
|
| 17 |
+
from firebase_config import get_db_ref
|
| 18 |
+
|
| 19 |
+
# ---------------- Setup ----------------
|
| 20 |
+
load_dotenv(find_dotenv())
|
| 21 |
+
set_tracing_disabled(True)
|
| 22 |
+
|
| 23 |
+
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
| 24 |
+
|
| 25 |
+
if not GEMINI_API_KEY:
|
| 26 |
+
raise RuntimeError("Missing Gemini API key in .env -> GEMINI_API_KEY")
|
| 27 |
+
|
| 28 |
+
client_provider = AsyncOpenAI(
|
| 29 |
+
api_key=GEMINI_API_KEY,
|
| 30 |
+
base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
|
| 31 |
+
)
|
| 32 |
+
Model = OpenAIChatCompletionsModel(
|
| 33 |
+
model="gemini-2.0-flash",
|
| 34 |
+
openai_client=client_provider,
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
runner = Runner()
|
| 38 |
+
|
| 39 |
+
# ---------------- Schemas ----------------
|
| 40 |
+
class SoilInput(BaseModel):
|
| 41 |
+
location: str
|
| 42 |
+
crop: Optional[str] = None
|
| 43 |
+
|
| 44 |
+
class SoilOutput(BaseModel):
|
| 45 |
+
location: str
|
| 46 |
+
crop: Optional[str]
|
| 47 |
+
soil_data: Dict[str, Any]
|
| 48 |
+
soil_health_score: float
|
| 49 |
+
nutrient_status: Dict[str, str]
|
| 50 |
+
recommendations: List[str]
|
| 51 |
+
fertilizer_plan: Dict[str, Any]
|
| 52 |
+
irrigation_advice: str
|
| 53 |
+
soil_type_suitability: str
|
| 54 |
+
deficiencies: List[str]
|
| 55 |
+
warnings: List[str]
|
| 56 |
+
summary: str
|
| 57 |
+
|
| 58 |
+
# ---------------- Helper functions ----------------
|
| 59 |
+
def fetch_soil_data(location: str, crop: Optional[str] = None) -> Dict[str, Any]:
|
| 60 |
+
"""Fetch soil data from Firebase for a specific location and optionally crop"""
|
| 61 |
+
try:
|
| 62 |
+
ref = get_db_ref("soil_data/latest")
|
| 63 |
+
all_data = ref.get() or {}
|
| 64 |
+
|
| 65 |
+
# Find matching soil data for location
|
| 66 |
+
matching_samples = []
|
| 67 |
+
for location_key, samples in all_data.items():
|
| 68 |
+
if isinstance(samples, dict):
|
| 69 |
+
for sample_id, sample in samples.items():
|
| 70 |
+
if isinstance(sample, dict):
|
| 71 |
+
sample_location = sample.get("location", "").lower()
|
| 72 |
+
sample_crop = sample.get("crop", "").lower()
|
| 73 |
+
|
| 74 |
+
# Match by location
|
| 75 |
+
if location.lower() in sample_location or sample_location in location.lower():
|
| 76 |
+
# If crop specified, filter by crop too
|
| 77 |
+
if crop:
|
| 78 |
+
if crop.lower() in sample_crop or sample_crop in crop.lower():
|
| 79 |
+
matching_samples.append(sample)
|
| 80 |
+
else:
|
| 81 |
+
matching_samples.append(sample)
|
| 82 |
+
|
| 83 |
+
if not matching_samples:
|
| 84 |
+
# If no exact match, get latest sample for any location
|
| 85 |
+
latest_ref = get_db_ref("soil_data/latest")
|
| 86 |
+
latest = latest_ref.get()
|
| 87 |
+
if latest and "sample" in latest:
|
| 88 |
+
return latest["sample"]
|
| 89 |
+
raise ValueError(f"No soil data found for location: {location}")
|
| 90 |
+
|
| 91 |
+
# Return the most recent sample
|
| 92 |
+
matching_samples.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
|
| 93 |
+
return matching_samples[0]
|
| 94 |
+
|
| 95 |
+
except Exception as e:
|
| 96 |
+
raise RuntimeError(f"Failed to fetch soil data: {str(e)}")
|
| 97 |
+
|
| 98 |
+
def calculate_soil_health_score(soil_data: Dict[str, Any]) -> float:
|
| 99 |
+
"""Calculate overall soil health score (0-100)"""
|
| 100 |
+
scores = []
|
| 101 |
+
|
| 102 |
+
# pH score (optimal range: 6.0-7.5)
|
| 103 |
+
ph = soil_data.get("ph", 7.0)
|
| 104 |
+
if 6.0 <= ph <= 7.5:
|
| 105 |
+
ph_score = 100
|
| 106 |
+
elif 5.5 <= ph < 6.0 or 7.5 < ph <= 8.0:
|
| 107 |
+
ph_score = 70
|
| 108 |
+
else:
|
| 109 |
+
ph_score = 40
|
| 110 |
+
scores.append(ph_score)
|
| 111 |
+
|
| 112 |
+
# Nutrient scores (N, P, K levels)
|
| 113 |
+
nutrient_levels = {
|
| 114 |
+
"nitrogen": soil_data.get("nitrogen_ppm", 0),
|
| 115 |
+
"phosphorus": soil_data.get("phosphorus_ppm", 0),
|
| 116 |
+
"potassium": soil_data.get("potassium_ppm", 0)
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
# Optimal ranges (ppm)
|
| 120 |
+
optimal_ranges = {
|
| 121 |
+
"nitrogen": (40, 80),
|
| 122 |
+
"phosphorus": (25, 50),
|
| 123 |
+
"potassium": (150, 300)
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
for nutrient, value in nutrient_levels.items():
|
| 127 |
+
min_val, max_val = optimal_ranges[nutrient]
|
| 128 |
+
if min_val <= value <= max_val:
|
| 129 |
+
scores.append(100)
|
| 130 |
+
elif value < min_val:
|
| 131 |
+
scores.append(max(40, (value / min_val) * 100))
|
| 132 |
+
else:
|
| 133 |
+
scores.append(max(60, 100 - ((value - max_val) / max_val) * 40))
|
| 134 |
+
|
| 135 |
+
# Organic matter score (optimal: >2%)
|
| 136 |
+
om = soil_data.get("organic_matter_percent", 2.0)
|
| 137 |
+
if om >= 2.5:
|
| 138 |
+
scores.append(100)
|
| 139 |
+
elif om >= 1.5:
|
| 140 |
+
scores.append(75)
|
| 141 |
+
else:
|
| 142 |
+
scores.append(50)
|
| 143 |
+
|
| 144 |
+
# Moisture score
|
| 145 |
+
moisture = soil_data.get("moisture_percent", 20)
|
| 146 |
+
if 15 <= moisture <= 25:
|
| 147 |
+
scores.append(100)
|
| 148 |
+
elif 10 <= moisture < 15 or 25 < moisture <= 35:
|
| 149 |
+
scores.append(70)
|
| 150 |
+
else:
|
| 151 |
+
scores.append(50)
|
| 152 |
+
|
| 153 |
+
return round(sum(scores) / len(scores), 2)
|
| 154 |
+
|
| 155 |
+
def analyze_nutrient_status(soil_data: Dict[str, Any]) -> Dict[str, str]:
|
| 156 |
+
"""Analyze nutrient levels and return status"""
|
| 157 |
+
status = {}
|
| 158 |
+
|
| 159 |
+
nitrogen = soil_data.get("nitrogen_ppm", 0)
|
| 160 |
+
status["nitrogen"] = "optimal" if 40 <= nitrogen <= 80 else ("low" if nitrogen < 40 else "high")
|
| 161 |
+
|
| 162 |
+
phosphorus = soil_data.get("phosphorus_ppm", 0)
|
| 163 |
+
status["phosphorus"] = "optimal" if 25 <= phosphorus <= 50 else ("low" if phosphorus < 25 else "high")
|
| 164 |
+
|
| 165 |
+
potassium = soil_data.get("potassium_ppm", 0)
|
| 166 |
+
status["potassium"] = "optimal" if 150 <= potassium <= 300 else ("low" if potassium < 150 else "high")
|
| 167 |
+
|
| 168 |
+
om = soil_data.get("organic_matter_percent", 0)
|
| 169 |
+
status["organic_matter"] = "optimal" if om >= 2.5 else ("moderate" if om >= 1.5 else "low")
|
| 170 |
+
|
| 171 |
+
return status
|
| 172 |
+
|
| 173 |
+
def generate_fertilizer_plan(soil_data: Dict[str, Any], nutrient_status: Dict[str, str]) -> Dict[str, Any]:
|
| 174 |
+
"""Generate fertilizer recommendations based on soil analysis"""
|
| 175 |
+
plan = {
|
| 176 |
+
"npk_ratio": "0-0-0",
|
| 177 |
+
"dosage_per_acre": "0 kg",
|
| 178 |
+
"application_method": "broadcast",
|
| 179 |
+
"timing": "pre-planting",
|
| 180 |
+
"organic_alternatives": []
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
n_status = nutrient_status.get("nitrogen", "optimal")
|
| 184 |
+
p_status = nutrient_status.get("phosphorus", "optimal")
|
| 185 |
+
k_status = nutrient_status.get("potassium", "optimal")
|
| 186 |
+
|
| 187 |
+
# Determine NPK ratio
|
| 188 |
+
n = 10 if n_status == "low" else (5 if n_status == "optimal" else 0)
|
| 189 |
+
p = 10 if p_status == "low" else (5 if p_status == "optimal" else 0)
|
| 190 |
+
k = 10 if k_status == "low" else (5 if k_status == "optimal" else 0)
|
| 191 |
+
|
| 192 |
+
plan["npk_ratio"] = f"{n}-{p}-{k}"
|
| 193 |
+
|
| 194 |
+
# Calculate dosage
|
| 195 |
+
total_units = n + p + k
|
| 196 |
+
plan["dosage_per_acre"] = f"{total_units * 10} kg" if total_units > 0 else "0 kg"
|
| 197 |
+
|
| 198 |
+
# Organic alternatives
|
| 199 |
+
if n_status == "low":
|
| 200 |
+
plan["organic_alternatives"].append("compost (2-3 tons/acre)")
|
| 201 |
+
if p_status == "low":
|
| 202 |
+
plan["organic_alternatives"].append("bone meal (50 kg/acre)")
|
| 203 |
+
if k_status == "low":
|
| 204 |
+
plan["organic_alternatives"].append("wood ash (30 kg/acre)")
|
| 205 |
+
|
| 206 |
+
return plan
|
| 207 |
+
|
| 208 |
+
# ---------------- Agent ----------------
|
| 209 |
+
soil_agent = Agent(
|
| 210 |
+
name="AgriSoilAgent",
|
| 211 |
+
model=Model,
|
| 212 |
+
instructions="""
|
| 213 |
+
You are an AgriTech Soil Analysis Agent for the Farmer-Centric Multi-Agent AgriTech System (AgriLedger+).
|
| 214 |
+
Your job is to analyze soil data and provide structured, actionable recommendations for farm operations.
|
| 215 |
+
|
| 216 |
+
Input: JSON containing soil sample data with:
|
| 217 |
+
- pH, nitrogen_ppm, phosphorus_ppm, potassium_ppm
|
| 218 |
+
- organic_matter_percent, moisture_percent
|
| 219 |
+
- soil_type, location, crop
|
| 220 |
+
|
| 221 |
+
Output: Strict JSON (no human-readable text) containing:
|
| 222 |
+
- location: string
|
| 223 |
+
- crop: optional string
|
| 224 |
+
- soil_data: raw soil measurements
|
| 225 |
+
- soil_health_score: 0-100 score
|
| 226 |
+
- nutrient_status: dict with status for each nutrient (low/optimal/high)
|
| 227 |
+
- recommendations: list of machine-readable instructions for automated operations
|
| 228 |
+
- fertilizer_plan: structured fertilizer recommendations
|
| 229 |
+
- irrigation_advice: water management guidance
|
| 230 |
+
- soil_type_suitability: assessment for specified crop
|
| 231 |
+
- deficiencies: list of critical deficiencies
|
| 232 |
+
- warnings: list of urgent actions needed
|
| 233 |
+
- summary: brief technical summary
|
| 234 |
+
|
| 235 |
+
Recommendations format examples:
|
| 236 |
+
- "fertilizer: apply NPK 10-10-10 @ 150kg/acre"
|
| 237 |
+
- "irrigation: increase frequency by 20%"
|
| 238 |
+
- "amendment: add lime @ 500kg/acre to raise pH"
|
| 239 |
+
- "organic_matter: add compost @ 2 tons/acre"
|
| 240 |
+
|
| 241 |
+
Constraints:
|
| 242 |
+
- Return strict JSON matching the schema
|
| 243 |
+
- Focus on actionable, measurable recommendations
|
| 244 |
+
- Include all fields even if empty/null
|
| 245 |
+
- No human-readable explanations outside JSON structure
|
| 246 |
+
""",
|
| 247 |
+
output_type=SoilOutput,
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
# ---------------- Main Flow ----------------
|
| 251 |
+
async def run_soil_agent(location: str, crop: Optional[str] = None):
|
| 252 |
+
"""Run soil analysis agent for a given location and optional crop"""
|
| 253 |
+
try:
|
| 254 |
+
soil_data = fetch_soil_data(location, crop)
|
| 255 |
+
except Exception as e:
|
| 256 |
+
print(f"โ Soil data fetch error: {e}")
|
| 257 |
+
return {"error": str(e)}
|
| 258 |
+
|
| 259 |
+
# Calculate derived metrics
|
| 260 |
+
health_score = calculate_soil_health_score(soil_data)
|
| 261 |
+
nutrient_status = analyze_nutrient_status(soil_data)
|
| 262 |
+
fertilizer_plan = generate_fertilizer_plan(soil_data, nutrient_status)
|
| 263 |
+
|
| 264 |
+
# Prepare message for agent
|
| 265 |
+
message = json.dumps({
|
| 266 |
+
"soil_data": soil_data,
|
| 267 |
+
"calculated_health_score": health_score,
|
| 268 |
+
"nutrient_status": nutrient_status,
|
| 269 |
+
"fertilizer_plan": fertilizer_plan
|
| 270 |
+
}, ensure_ascii=False)
|
| 271 |
+
|
| 272 |
+
try:
|
| 273 |
+
resp = await runner.run(
|
| 274 |
+
soil_agent,
|
| 275 |
+
message,
|
| 276 |
+
session=SQLiteSession("trace.db")
|
| 277 |
+
)
|
| 278 |
+
if hasattr(resp, "output"):
|
| 279 |
+
agent_output = resp.output.model_dump()
|
| 280 |
+
elif hasattr(resp, "final_output"):
|
| 281 |
+
agent_output = resp.final_output.model_dump()
|
| 282 |
+
else:
|
| 283 |
+
agent_output = {}
|
| 284 |
+
except Exception as e:
|
| 285 |
+
print(f"โ Agent run failed: {e}")
|
| 286 |
+
agent_output = {}
|
| 287 |
+
|
| 288 |
+
# Fallback if agent fails
|
| 289 |
+
if not agent_output:
|
| 290 |
+
deficiencies = []
|
| 291 |
+
warnings = []
|
| 292 |
+
recommendations = []
|
| 293 |
+
|
| 294 |
+
for nutrient, status in nutrient_status.items():
|
| 295 |
+
if status == "low":
|
| 296 |
+
deficiencies.append(f"{nutrient} deficiency detected")
|
| 297 |
+
recommendations.append(f"fertilizer: apply {nutrient} supplement")
|
| 298 |
+
|
| 299 |
+
ph = soil_data.get("ph", 7.0)
|
| 300 |
+
if ph < 6.0:
|
| 301 |
+
warnings.append("acidic soil detected")
|
| 302 |
+
recommendations.append("amendment: add lime to raise pH")
|
| 303 |
+
elif ph > 8.0:
|
| 304 |
+
warnings.append("alkaline soil detected")
|
| 305 |
+
recommendations.append("amendment: add sulfur to lower pH")
|
| 306 |
+
|
| 307 |
+
moisture = soil_data.get("moisture_percent", 20)
|
| 308 |
+
if moisture < 15:
|
| 309 |
+
warnings.append("low soil moisture")
|
| 310 |
+
recommendations.append("irrigation: increase immediately")
|
| 311 |
+
|
| 312 |
+
agent_output = {
|
| 313 |
+
"location": soil_data.get("location", location),
|
| 314 |
+
"crop": crop or soil_data.get("crop"),
|
| 315 |
+
"soil_data": soil_data,
|
| 316 |
+
"soil_health_score": health_score,
|
| 317 |
+
"nutrient_status": nutrient_status,
|
| 318 |
+
"recommendations": recommendations or ["monitor: normal"],
|
| 319 |
+
"fertilizer_plan": fertilizer_plan,
|
| 320 |
+
"irrigation_advice": "moderate watering" if 15 <= moisture <= 25 else ("increase watering" if moisture < 15 else "reduce watering"),
|
| 321 |
+
"soil_type_suitability": "suitable for most crops",
|
| 322 |
+
"deficiencies": deficiencies,
|
| 323 |
+
"warnings": warnings,
|
| 324 |
+
"summary": f"Soil health: {health_score}/100. " + (", ".join(warnings) if warnings else "No critical issues.")
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
print(json.dumps(agent_output, indent=2, ensure_ascii=False))
|
| 328 |
+
return agent_output
|
| 329 |
+
|
| 330 |
+
# ---------------- Run standalone ----------------
|
| 331 |
+
if __name__ == "__main__":
|
| 332 |
+
location_name = input("Enter location name: ")
|
| 333 |
+
crop_name = input("Enter crop name (optional, press Enter to skip): ").strip() or None
|
| 334 |
+
asyncio.run(run_soil_agent(location_name, crop_name))
|
ageents/weatherAgent.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import asyncio
|
| 4 |
+
from dotenv import load_dotenv, find_dotenv
|
| 5 |
+
from pydantic import BaseModel
|
| 6 |
+
from typing import Dict, Any, List, Optional
|
| 7 |
+
import requests
|
| 8 |
+
|
| 9 |
+
from agents import (
|
| 10 |
+
Agent,
|
| 11 |
+
Runner,
|
| 12 |
+
AsyncOpenAI,
|
| 13 |
+
OpenAIChatCompletionsModel,
|
| 14 |
+
set_tracing_disabled,
|
| 15 |
+
SQLiteSession,
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
# ---------------- Setup ----------------
|
| 19 |
+
load_dotenv(find_dotenv())
|
| 20 |
+
set_tracing_disabled(True)
|
| 21 |
+
|
| 22 |
+
WEATHER_API_KEY = os.getenv("API_KEY")
|
| 23 |
+
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
| 24 |
+
|
| 25 |
+
if not WEATHER_API_KEY:
|
| 26 |
+
raise RuntimeError("Missing OpenWeatherMap API key in .env -> API_KEY")
|
| 27 |
+
if not GEMINI_API_KEY:
|
| 28 |
+
raise RuntimeError("Missing Gemini API key in .env -> GEMINI_API_KEY")
|
| 29 |
+
|
| 30 |
+
client_provider = AsyncOpenAI(
|
| 31 |
+
api_key=GEMINI_API_KEY,
|
| 32 |
+
base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
|
| 33 |
+
)
|
| 34 |
+
Model = OpenAIChatCompletionsModel(
|
| 35 |
+
model="gemini-2.0-flash",
|
| 36 |
+
openai_client=client_provider,
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
runner = Runner()
|
| 40 |
+
|
| 41 |
+
# ---------------- Schemas ----------------
|
| 42 |
+
class WeatherInput(BaseModel):
|
| 43 |
+
city: str
|
| 44 |
+
|
| 45 |
+
class WeatherOutput(BaseModel):
|
| 46 |
+
city: str
|
| 47 |
+
current: Dict[str, Any]
|
| 48 |
+
raw_forecast: List[Dict[str, Any]]
|
| 49 |
+
daily_summary: Dict[str, Dict[str, Any]]
|
| 50 |
+
recommendations: Optional[List[str]] = []
|
| 51 |
+
trend: Optional[str] = None
|
| 52 |
+
precipitation_chance: Optional[float] = None
|
| 53 |
+
summary: Optional[str] = None
|
| 54 |
+
|
| 55 |
+
# ---------------- Helper functions ----------------
|
| 56 |
+
def fetch_current(city: str) -> Dict[str, Any]:
|
| 57 |
+
url = f"http://api.openweathermap.org/data/2.5/weather?q={city}&appid={WEATHER_API_KEY}&units=metric"
|
| 58 |
+
r = requests.get(url, timeout=10)
|
| 59 |
+
r.raise_for_status()
|
| 60 |
+
return r.json()
|
| 61 |
+
|
| 62 |
+
def fetch_forecast(city: str) -> Dict[str, Any]:
|
| 63 |
+
url = f"http://api.openweathermap.org/data/2.5/forecast?q={city}&appid={WEATHER_API_KEY}&units=metric"
|
| 64 |
+
r = requests.get(url, timeout=10)
|
| 65 |
+
r.raise_for_status()
|
| 66 |
+
return r.json()
|
| 67 |
+
|
| 68 |
+
def aggregate_daily_forecast(forecast_data: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
|
| 69 |
+
from collections import defaultdict, Counter
|
| 70 |
+
daily_data = defaultdict(list)
|
| 71 |
+
for entry in forecast_data.get("list", []):
|
| 72 |
+
date = entry["dt_txt"].split(" ")[0]
|
| 73 |
+
daily_data[date].append(entry)
|
| 74 |
+
|
| 75 |
+
structured_forecast = {}
|
| 76 |
+
for date, entries in daily_data.items():
|
| 77 |
+
temps = [e['main']['temp'] for e in entries]
|
| 78 |
+
feels_like = [e['main']['feels_like'] for e in entries]
|
| 79 |
+
humidity = [e['main']['humidity'] for e in entries]
|
| 80 |
+
wind_speed = [e['wind']['speed'] for e in entries]
|
| 81 |
+
descriptions = [e['weather'][0]['description'].title() for e in entries]
|
| 82 |
+
most_common_desc = Counter(descriptions).most_common(1)[0][0]
|
| 83 |
+
|
| 84 |
+
structured_forecast[date] = {
|
| 85 |
+
"temperature": f"{sum(temps)/len(temps):.2f} ยฐC",
|
| 86 |
+
"feels_like": f"{sum(feels_like)/len(feels_like):.2f} ยฐC",
|
| 87 |
+
"description": most_common_desc,
|
| 88 |
+
"humidity": f"{sum(humidity)/len(humidity):.0f}%",
|
| 89 |
+
"wind_speed": f"{sum(wind_speed)/len(wind_speed):.2f} m/s"
|
| 90 |
+
}
|
| 91 |
+
return structured_forecast
|
| 92 |
+
|
| 93 |
+
# ---------------- Agent ----------------
|
| 94 |
+
weather_agent = Agent(
|
| 95 |
+
name="AgriWeatherAgent",
|
| 96 |
+
model=Model,
|
| 97 |
+
instructions="""
|
| 98 |
+
You are an AgriTech Weather Agent for the Farmer-Centric Multi-Agent AgriTech System (AgriLedger+).
|
| 99 |
+
Your job is to process raw weather data from OpenWeatherMap and generate structured outputs for automated farm operations and multi-agent advisory use.
|
| 100 |
+
|
| 101 |
+
Input: JSON containing:
|
| 102 |
+
- "current": current weather data
|
| 103 |
+
- "forecast": 5-day / 3-hour interval forecast
|
| 104 |
+
|
| 105 |
+
Output: Strict JSON (no human-readable text) containing:
|
| 106 |
+
- city: string
|
| 107 |
+
- current:
|
| 108 |
+
- temperature: float ยฐC
|
| 109 |
+
- feels_like: float ยฐC
|
| 110 |
+
- description: weather description
|
| 111 |
+
- humidity: % value
|
| 112 |
+
- wind_speed: m/s
|
| 113 |
+
- raw_forecast: list of 3-hour interval forecast entries as returned by OpenWeatherMap
|
| 114 |
+
- daily_summary: aggregated daily forecast with:
|
| 115 |
+
- temperature, feels_like, description, humidity, wind_speed
|
| 116 |
+
- trend: one of "warming", "cooling", "stable" based on temperature over next 3 days
|
| 117 |
+
- precipitation_chance: estimated % chance of rain over next 3 days
|
| 118 |
+
- recommendations: structured machine-readable instructions for automated farm operations only, e.g.,
|
| 119 |
+
- "irrigation: postpone"
|
| 120 |
+
- "fertilizer: delay"
|
| 121 |
+
- "pesticide: apply"
|
| 122 |
+
|
| 123 |
+
Constraints:
|
| 124 |
+
- Do NOT generate any human-readable summaries or explanations.
|
| 125 |
+
- Return strict JSON that matches the schema above.
|
| 126 |
+
- Include all fields even if empty; null is acceptable.
|
| 127 |
+
""",
|
| 128 |
+
output_type=WeatherOutput,
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
# ---------------- Main Flow ----------------
|
| 132 |
+
async def run_weather_agent(city: str):
|
| 133 |
+
try:
|
| 134 |
+
current = fetch_current(city)
|
| 135 |
+
forecast = fetch_forecast(city)
|
| 136 |
+
except Exception as e:
|
| 137 |
+
print(f"โ OpenWeatherMap API error: {e}")
|
| 138 |
+
return {}
|
| 139 |
+
|
| 140 |
+
daily_summary = aggregate_daily_forecast(forecast)
|
| 141 |
+
message = json.dumps({
|
| 142 |
+
"current": current,
|
| 143 |
+
"forecast": forecast
|
| 144 |
+
}, ensure_ascii=False)
|
| 145 |
+
|
| 146 |
+
try:
|
| 147 |
+
resp = await runner.run(
|
| 148 |
+
weather_agent,
|
| 149 |
+
message,
|
| 150 |
+
session=SQLiteSession("trace.db")
|
| 151 |
+
)
|
| 152 |
+
if hasattr(resp, "output"):
|
| 153 |
+
agent_output = resp.output.model_dump()
|
| 154 |
+
elif hasattr(resp, "final_output"):
|
| 155 |
+
agent_output = resp.final_output.model_dump()
|
| 156 |
+
else:
|
| 157 |
+
agent_output = {}
|
| 158 |
+
except Exception as e:
|
| 159 |
+
print(f"โ Agent run failed: {e}")
|
| 160 |
+
agent_output = {}
|
| 161 |
+
|
| 162 |
+
# --- Fallback if agent fails ---
|
| 163 |
+
if not agent_output:
|
| 164 |
+
agent_output = {
|
| 165 |
+
"city": current.get("name", city),
|
| 166 |
+
"current": {
|
| 167 |
+
"temperature": f"{current.get('main', {}).get('temp')} ยฐC",
|
| 168 |
+
"feels_like": f"{current.get('main', {}).get('feels_like')} ยฐC",
|
| 169 |
+
"description": current.get('weather',[{}])[0].get('description','').title(),
|
| 170 |
+
"humidity": f"{current.get('main', {}).get('humidity')}%",
|
| 171 |
+
"wind_speed": f"{current.get('wind',{}).get('speed')} m/s"
|
| 172 |
+
},
|
| 173 |
+
"raw_forecast": forecast.get("list", []),
|
| 174 |
+
"daily_summary": daily_summary,
|
| 175 |
+
"recommendations": []
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
# --- Add computed trend, precipitation chance & summary ---
|
| 179 |
+
try:
|
| 180 |
+
temps = []
|
| 181 |
+
for d in list(daily_summary.values())[:3]:
|
| 182 |
+
t = d["temperature"].split(" ")[0]
|
| 183 |
+
try:
|
| 184 |
+
temps.append(float(t))
|
| 185 |
+
except:
|
| 186 |
+
pass
|
| 187 |
+
|
| 188 |
+
# Compute trend
|
| 189 |
+
if len(temps) >= 2:
|
| 190 |
+
diff = temps[-1] - temps[0]
|
| 191 |
+
if diff > 1:
|
| 192 |
+
trend = "warming"
|
| 193 |
+
elif diff < -1:
|
| 194 |
+
trend = "cooling"
|
| 195 |
+
else:
|
| 196 |
+
trend = "stable"
|
| 197 |
+
else:
|
| 198 |
+
trend = "stable"
|
| 199 |
+
|
| 200 |
+
# Estimate precipitation chance
|
| 201 |
+
forecast_list = forecast.get("list", [])
|
| 202 |
+
rainy = [f for f in forecast_list if "rain" in f.get("weather", [{}])[0].get("description", "").lower()]
|
| 203 |
+
precipitation_chance = round(len(rainy) / len(forecast_list) * 100, 2) if forecast_list else 0
|
| 204 |
+
|
| 205 |
+
# Add default recommendations
|
| 206 |
+
recommendations = agent_output.get("recommendations", [])
|
| 207 |
+
if not recommendations:
|
| 208 |
+
if trend == "warming":
|
| 209 |
+
recommendations = ["irrigation: increase"]
|
| 210 |
+
elif trend == "cooling":
|
| 211 |
+
recommendations = ["irrigation: reduce"]
|
| 212 |
+
elif precipitation_chance > 40:
|
| 213 |
+
recommendations = ["fertilizer: delay", "irrigation: postpone"]
|
| 214 |
+
else:
|
| 215 |
+
recommendations = ["monitor: normal"]
|
| 216 |
+
|
| 217 |
+
agent_output["trend"] = trend
|
| 218 |
+
agent_output["precipitation_chance"] = precipitation_chance
|
| 219 |
+
agent_output["recommendations"] = recommendations
|
| 220 |
+
agent_output["summary"] = (
|
| 221 |
+
f"Trend: {trend.capitalize()}, "
|
| 222 |
+
f"Rain chance: {precipitation_chance}%. "
|
| 223 |
+
f"Recommended actions: {', '.join(recommendations)}."
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
except Exception as e:
|
| 227 |
+
agent_output["trend"] = "unknown"
|
| 228 |
+
agent_output["precipitation_chance"] = None
|
| 229 |
+
agent_output["summary"] = f"Summary unavailable due to error: {str(e)}"
|
| 230 |
+
|
| 231 |
+
print(json.dumps(agent_output, indent=2, ensure_ascii=False))
|
| 232 |
+
return agent_output
|
| 233 |
+
|
| 234 |
+
# ---------------- Run standalone ----------------
|
| 235 |
+
if __name__ == "__main__":
|
| 236 |
+
city_name = input("Enter city name: ")
|
| 237 |
+
asyncio.run(run_weather_agent(city_name))
|
firebase_config.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import firebase_admin
|
| 2 |
+
from firebase_admin import credentials, db
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
SERVICE_ACCOUNT_KEY = os.path.join(os.path.dirname(__file__), "firebase_key.json")
|
| 6 |
+
|
| 7 |
+
if not firebase_admin._apps:
|
| 8 |
+
cred = credentials.Certificate(SERVICE_ACCOUNT_KEY)
|
| 9 |
+
firebase_admin.initialize_app(cred, {
|
| 10 |
+
"databaseURL": "https://fintech-backend-1f7c0-default-rtdb.firebaseio.com"
|
| 11 |
+
})
|
| 12 |
+
|
| 13 |
+
def get_db_ref(path: str = "/"):
|
| 14 |
+
"""
|
| 15 |
+
Get a reference to a specific path in Firebase Realtime Database.
|
| 16 |
+
Example: get_db_ref("entries") โ /entries
|
| 17 |
+
"""
|
| 18 |
+
return db.reference(path)
|
firebase_key.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"type": "service_account",
|
| 3 |
+
"project_id": "fintech-backend-1f7c0",
|
| 4 |
+
"private_key_id": "d63430dac92893561935331d751dd0304a8b3d07",
|
| 5 |
+
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCMS+uta8AqhkRG\nA81wycLVc9nlQRRw5LyhYODpTI6b7WRl8O2Be4xbjhyM1Um2kD6hLrCT1dbgJJKg\nSPxYuzc9BGWzpITmIfFX2V/SfcN3Xlm3BgzWdOVefjZuEe+QXqBMhwoebjPasRki\nfNdqObjXIW4o298kH5wcGXx3rLwRJjjFJKaVZw8CWHDGlsDI81WRI2X4f7Qvbk01\nGq5HFF8E7CXpLT5IhcowIMeEZPURFi8tyF2Cutv/xG6p5T3lJSfKQE1PltG3kpHx\n6+3Kjrm8ig7sgyNYG8HTJQos4/6iAhzSJFbcFuXjKR/J7zvdxsjyiducgn0I4b1r\nn9V6+cCjAgMBAAECggEAHwu74vv5oc9lc1LrFt7m+QIygStWISdU4KdGDgptcm7l\nbTOT8zfKVwZLJhUaw0YHPfbNh2FmM+KT+dk80kvXjO6YrNimuiBfvk9xh2xiIVul\nIb26gIiMq0zlsZTRfRKbiWPGpvY/DlFrXPjgWHbegui4bl09c0p1arhvFTKJyYdm\nXyQarg3mzWm01aiKjV3rYdhTKZKlONq81vc+snII9fCw3eddfYkvGhyePmH50niE\nHQOT40YSjWNaliYuK01yygszRo6my1hWQcjV8yx4YXKRzuOxwJmmPk2NqsrZ+32+\nSlJuD7IhoUyvkJ77oq2jmhuoTs0GoNA4Ijo/rmtB4QKBgQC+islrf6GZ8DHZuyGC\n3JlKOcLwU9VIeneomVNLSo8WH5jKqm1ALMWspWo4HtoM/O06XqVa3OysT4K0wJK3\nanun9bWTnAjD0xUhso9+vxopKJCLv1Esbhrkm2SOgOzufwp2XOlutRAzWNiuQV2U\nedUHoJh8/BwcVcyVLjHbmgHBYQKBgQC8fkp/7zMxvXkrz3ptip8IgyaB6ct6W7BC\nsofspKcv9CsaqcMMXT7fdoGh/j9OxV0d6V1MpeT16e2LLWbj50GEkeoDyGdB7BiC\nt+NfD4IGByWYFhIinZSJKMBMW6FCkAtzcS8Suic7viBhDwG5A2HiptqVDH7jk0Ry\nu54vh4tMgwKBgCoqfm5qnTF6xR1g2wahmM6jP699bdqPN4G5BITJ6CZGMcLPukOU\nZN5S8NGgijKLmlfrb/5Om5V6NvuXDiDG0LyXlGopAouLX8bIRBcRZVGsZ2h1mxoQ\n96SVeshKYaRZus/8ua/FI+OpCrJtRq8/0tPQR06JYBMpLY/p3CCz0SWhAoGBAJCN\n+exrMUOwH0Et+KIRcS1CB0ISXm4T6vT7naoqC92Gz5e/IUpWKGWPqJLnPP3X9jV+\nRoMFprXBFN9rjkCxlVlp3aHRCv9PZOy6wbChYAHncTlVk8rgNo2Jpw/oJZ+6gE2k\nO4mNIZF7mbFVpOiSY3tCotczogw9YHzszb99n6KpAoGAXS76tsid/Ga4Cyfa4SZA\n5V9ICEytoc0XWKZoZysb7ws1ryWJNvzodVkbMWXWSbeYDVENfZYFepf77DZofUKj\ntzvqN4eNg/QjZH3W076QGZg7yt1KIYJw0l+2Vv6HZu1xY5b27Hdr3vcmjKLFgSsd\nRMPPg5wx6kTUDUNw0Qxp68U=\n-----END PRIVATE KEY-----\n",
|
| 6 |
+
"client_email": "firebase-adminsdk-fbsvc@fintech-backend-1f7c0.iam.gserviceaccount.com",
|
| 7 |
+
"client_id": "115784013323321037308",
|
| 8 |
+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
| 9 |
+
"token_uri": "https://oauth2.googleapis.com/token",
|
| 10 |
+
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
| 11 |
+
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40fintech-backend-1f7c0.iam.gserviceaccount.com",
|
| 12 |
+
"universe_domain": "googleapis.com"
|
| 13 |
+
}
|
main.py
ADDED
|
@@ -0,0 +1,2264 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
AgriLedger+ FastAPI Backend - Complete Implementation
|
| 3 |
+
Includes entries management and market advisory with seeded data
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import secrets
|
| 8 |
+
import re
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
from dotenv import load_dotenv
|
| 11 |
+
from fastapi import FastAPI, HTTPException, Query, Body
|
| 12 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 13 |
+
from typing import Optional, Dict, List
|
| 14 |
+
|
| 15 |
+
from models.expense import EntryCreate, EntryResponse
|
| 16 |
+
from models.market import (
|
| 17 |
+
AdviceRequest,
|
| 18 |
+
MarketAdviceResponse,
|
| 19 |
+
CropSummary,
|
| 20 |
+
MarketSummaryResponse,
|
| 21 |
+
HealthStatus,
|
| 22 |
+
InitializationResponse
|
| 23 |
+
)
|
| 24 |
+
from firebase_config import get_db_ref
|
| 25 |
+
from ageents.marketAgent import get_market_advice
|
| 26 |
+
import bcrypt
|
| 27 |
+
|
| 28 |
+
load_dotenv()
|
| 29 |
+
|
| 30 |
+
# -------------------- INIT APP --------------------
|
| 31 |
+
app = FastAPI(
|
| 32 |
+
title="AgriLedger+ API",
|
| 33 |
+
description="AI-powered agricultural ledger and market advisory system for Pakistani farmers",
|
| 34 |
+
version="3.0.0"
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
app.add_middleware(
|
| 38 |
+
CORSMiddleware,
|
| 39 |
+
allow_origins=["*"],
|
| 40 |
+
allow_credentials=True,
|
| 41 |
+
allow_methods=["*"],
|
| 42 |
+
allow_headers=["*"],
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
# -------------------- UTILITIES --------------------
|
| 46 |
+
def generate_short_id(length: int = 7) -> str:
|
| 47 |
+
"""Generate short unique ID"""
|
| 48 |
+
alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz"
|
| 49 |
+
return "".join(secrets.choice(alphabet) for _ in range(length))
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def parse_advice_to_structured(raw_advice: str, crop: str) -> MarketAdviceResponse:
|
| 53 |
+
"""Parse raw agent advice into structured response"""
|
| 54 |
+
|
| 55 |
+
# Extract action
|
| 56 |
+
action_match = re.search(r'\*\*Action:\*\*\s*([A-Z\s]+)', raw_advice, re.IGNORECASE)
|
| 57 |
+
action = action_match.group(1).strip() if action_match else "HOLD"
|
| 58 |
+
|
| 59 |
+
# Extract current price
|
| 60 |
+
price_match = re.search(r'โจ([\d,]+(?:\.\d{2})?)', raw_advice)
|
| 61 |
+
current_price = float(price_match.group(1).replace(',', '')) if price_match else 0.0
|
| 62 |
+
|
| 63 |
+
# Extract price trend
|
| 64 |
+
trend_match = re.search(r'Trend:\s*(\w+)\s*by\s*([-+]?\d+\.?\d*)%', raw_advice, re.IGNORECASE)
|
| 65 |
+
if trend_match:
|
| 66 |
+
price_trend = f"{trend_match.group(1)} by {trend_match.group(2)}%"
|
| 67 |
+
else:
|
| 68 |
+
price_trend = "stable"
|
| 69 |
+
|
| 70 |
+
# Extract confidence
|
| 71 |
+
confidence_match = re.search(r'\*\*Confidence:\*\*\s*(\w+)', raw_advice, re.IGNORECASE)
|
| 72 |
+
confidence = confidence_match.group(1).upper() if confidence_match else "MEDIUM"
|
| 73 |
+
|
| 74 |
+
# Extract seasonal context
|
| 75 |
+
seasonal_match = re.search(r'Seasonal Context:\s*([^\nโข]+)', raw_advice, re.IGNORECASE)
|
| 76 |
+
seasonal_context = seasonal_match.group(1).strip() if seasonal_match else "N/A"
|
| 77 |
+
|
| 78 |
+
# Extract recommendation
|
| 79 |
+
rec_match = re.search(r'\*\*Recommendation:\*\*\s*([^\n]+)', raw_advice, re.IGNORECASE)
|
| 80 |
+
reasoning = rec_match.group(1).strip() if rec_match else "Monitor market conditions"
|
| 81 |
+
|
| 82 |
+
# Extract risk factors
|
| 83 |
+
risk_section = re.search(r'\*\*Risk Factors:\*\*\s*([\s\S]+?)(?:\n\n|\*|\Z)', raw_advice, re.IGNORECASE)
|
| 84 |
+
risk_factors = []
|
| 85 |
+
if risk_section:
|
| 86 |
+
risks = re.findall(r'โข\s*([^\n]+)', risk_section.group(1))
|
| 87 |
+
risk_factors = [r.strip() for r in risks[:3]]
|
| 88 |
+
|
| 89 |
+
if not risk_factors:
|
| 90 |
+
risk_factors = ["Market volatility", "Seasonal price fluctuations", "Weather conditions"]
|
| 91 |
+
|
| 92 |
+
return MarketAdviceResponse(
|
| 93 |
+
crop=crop,
|
| 94 |
+
action=action,
|
| 95 |
+
current_price=current_price,
|
| 96 |
+
price_trend=price_trend,
|
| 97 |
+
confidence=confidence,
|
| 98 |
+
reasoning=reasoning,
|
| 99 |
+
risk_factors=risk_factors,
|
| 100 |
+
seasonal_context=seasonal_context,
|
| 101 |
+
timestamp=datetime.utcnow().isoformat(),
|
| 102 |
+
raw_advice=raw_advice
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
# ================== AUTH ENDPOINTS ==================
|
| 106 |
+
|
| 107 |
+
@app.post("/auth/signup", tags=["auth"])
|
| 108 |
+
async def signup(
|
| 109 |
+
name: str = Body(...),
|
| 110 |
+
phone: str = Body(...),
|
| 111 |
+
email: str = Body(...),
|
| 112 |
+
password: str = Body(...),
|
| 113 |
+
city: str = Body(...),
|
| 114 |
+
state: str = Body(...),
|
| 115 |
+
country: str = Body(...),
|
| 116 |
+
land_area: float = Body(...),
|
| 117 |
+
unit: str = Body(...)
|
| 118 |
+
):
|
| 119 |
+
"""User signup - stores user info securely in Firebase"""
|
| 120 |
+
try:
|
| 121 |
+
ref = get_db_ref("users")
|
| 122 |
+
|
| 123 |
+
# Check if user already exists
|
| 124 |
+
all_users = ref.get() or {}
|
| 125 |
+
for _, user in all_users.items():
|
| 126 |
+
if user.get("email") == email:
|
| 127 |
+
raise HTTPException(status_code=400, detail="User already exists with this email")
|
| 128 |
+
|
| 129 |
+
# Hash password securely
|
| 130 |
+
hashed_pw = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
|
| 131 |
+
|
| 132 |
+
user_id = generate_short_id(8)
|
| 133 |
+
user_data = {
|
| 134 |
+
"name": name,
|
| 135 |
+
"phone": phone,
|
| 136 |
+
"email": email,
|
| 137 |
+
"password": hashed_pw,
|
| 138 |
+
"location": {
|
| 139 |
+
"city": city,
|
| 140 |
+
"state": state,
|
| 141 |
+
"country": country
|
| 142 |
+
},
|
| 143 |
+
"land_info": {
|
| 144 |
+
"area": land_area,
|
| 145 |
+
"unit": unit
|
| 146 |
+
},
|
| 147 |
+
"createdAt": datetime.utcnow().isoformat()
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
ref.child(user_id).set(user_data)
|
| 151 |
+
|
| 152 |
+
# Don't return password in response
|
| 153 |
+
safe_data = {k: v for k, v in user_data.items() if k != "password"}
|
| 154 |
+
|
| 155 |
+
return {
|
| 156 |
+
"message": "Signup successful",
|
| 157 |
+
"user_id": user_id,
|
| 158 |
+
"data": safe_data
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
except HTTPException:
|
| 162 |
+
raise
|
| 163 |
+
except Exception as e:
|
| 164 |
+
raise HTTPException(status_code=500, detail=f"Error saving user: {str(e)}")
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
@app.post("/auth/login", tags=["auth"])
|
| 168 |
+
async def login(email: str = Body(...), password: str = Body(...)):
|
| 169 |
+
"""User login - verifies email and password (no Firebase index needed)"""
|
| 170 |
+
try:
|
| 171 |
+
ref = get_db_ref("users")
|
| 172 |
+
|
| 173 |
+
# โ
Manual lookup instead of using order_by_child (no .indexOn required)
|
| 174 |
+
all_users = ref.get() or {}
|
| 175 |
+
found_user = None
|
| 176 |
+
found_user_id = None
|
| 177 |
+
|
| 178 |
+
for uid, user in all_users.items():
|
| 179 |
+
if user.get("email") == email:
|
| 180 |
+
found_user = user
|
| 181 |
+
found_user_id = uid
|
| 182 |
+
break
|
| 183 |
+
|
| 184 |
+
if not found_user:
|
| 185 |
+
raise HTTPException(status_code=404, detail="User not found")
|
| 186 |
+
|
| 187 |
+
stored_pw = found_user.get("password")
|
| 188 |
+
|
| 189 |
+
# โ
Verify password securely
|
| 190 |
+
if not bcrypt.checkpw(password.encode('utf-8'), stored_pw.encode('utf-8')):
|
| 191 |
+
raise HTTPException(status_code=400, detail="Incorrect password")
|
| 192 |
+
|
| 193 |
+
# โ
Remove password before sending response
|
| 194 |
+
safe_data = {k: v for k, v in found_user.items() if k != "password"}
|
| 195 |
+
|
| 196 |
+
return {
|
| 197 |
+
"message": "Login successful",
|
| 198 |
+
"user_id": found_user_id,
|
| 199 |
+
"data": safe_data
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
except HTTPException:
|
| 203 |
+
raise
|
| 204 |
+
except Exception as e:
|
| 205 |
+
raise HTTPException(status_code=500, detail=f"Login error: {str(e)}")
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
# ================== ENTRIES ENDPOINTS ==================
|
| 210 |
+
|
| 211 |
+
@app.post("/entries", response_model=EntryResponse, tags=["entries"])
|
| 212 |
+
async def create_entry(payload: EntryCreate):
|
| 213 |
+
"""Create a new ledger entry"""
|
| 214 |
+
now_iso = datetime.utcnow().isoformat()
|
| 215 |
+
entry_id = generate_short_id(7)
|
| 216 |
+
|
| 217 |
+
entry = {
|
| 218 |
+
"type": "entry",
|
| 219 |
+
"entryType": payload.entryType,
|
| 220 |
+
"category": payload.category,
|
| 221 |
+
"amount": payload.amount,
|
| 222 |
+
"currency": payload.currency or "PKR",
|
| 223 |
+
"paymentMethod": payload.paymentMethod or "cash",
|
| 224 |
+
"notes": payload.notes,
|
| 225 |
+
"createdAt": payload.date or now_iso,
|
| 226 |
+
"recordedBy": payload.recordedBy,
|
| 227 |
+
"deviceId": payload.deviceId,
|
| 228 |
+
"syncStatus": "synced",
|
| 229 |
+
"meta": payload.meta.model_dump() if payload.meta else None,
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
try:
|
| 233 |
+
ref = get_db_ref("entries")
|
| 234 |
+
ref.child(entry_id).set(entry)
|
| 235 |
+
print(f"โ
Entry saved: {entry_id}")
|
| 236 |
+
return {"id": entry_id, **entry}
|
| 237 |
+
except Exception as e:
|
| 238 |
+
raise HTTPException(status_code=500, detail=f"Firebase error: {str(e)}")
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
@app.get("/entries", tags=["entries"])
|
| 242 |
+
async def list_entries(
|
| 243 |
+
limit: Optional[int] = Query(None, ge=1, le=1000),
|
| 244 |
+
skip: Optional[int] = Query(None, ge=0),
|
| 245 |
+
entry_type: Optional[str] = Query(None, description="Filter by entryType"),
|
| 246 |
+
category: Optional[str] = Query(None, description="Filter by category")
|
| 247 |
+
):
|
| 248 |
+
"""List all ledger entries with optional filters"""
|
| 249 |
+
try:
|
| 250 |
+
ref = get_db_ref("entries")
|
| 251 |
+
data = ref.get() or {}
|
| 252 |
+
entries = [{"id": k, **v} for k, v in data.items()]
|
| 253 |
+
|
| 254 |
+
# Apply filters
|
| 255 |
+
if entry_type:
|
| 256 |
+
entries = [e for e in entries if e.get("entryType") == entry_type]
|
| 257 |
+
if category:
|
| 258 |
+
entries = [e for e in entries if e.get("category") == category]
|
| 259 |
+
|
| 260 |
+
# Sort by date (newest first)
|
| 261 |
+
entries.sort(key=lambda x: x.get("createdAt", ""), reverse=True)
|
| 262 |
+
|
| 263 |
+
# Apply pagination
|
| 264 |
+
if skip:
|
| 265 |
+
entries = entries[skip:]
|
| 266 |
+
if limit:
|
| 267 |
+
entries = entries[:limit]
|
| 268 |
+
|
| 269 |
+
return {
|
| 270 |
+
"total": len(entries),
|
| 271 |
+
"entries": entries
|
| 272 |
+
}
|
| 273 |
+
except Exception as e:
|
| 274 |
+
raise HTTPException(status_code=500, detail=f"Firebase error: {str(e)}")
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
@app.get("/entries/{entry_id}", tags=["entries"])
|
| 278 |
+
async def get_entry(entry_id: str):
|
| 279 |
+
"""Get a specific entry by ID"""
|
| 280 |
+
try:
|
| 281 |
+
ref = get_db_ref(f"entries/{entry_id}")
|
| 282 |
+
entry = ref.get()
|
| 283 |
+
if not entry:
|
| 284 |
+
raise HTTPException(status_code=404, detail="Entry not found")
|
| 285 |
+
return {"id": entry_id, **entry}
|
| 286 |
+
except HTTPException:
|
| 287 |
+
raise
|
| 288 |
+
except Exception as e:
|
| 289 |
+
raise HTTPException(status_code=500, detail=f"Firebase error: {str(e)}")
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
@app.put("/entries/{entry_id}", tags=["entries"])
|
| 293 |
+
async def update_entry(entry_id: str, payload: EntryCreate):
|
| 294 |
+
"""Update an existing entry"""
|
| 295 |
+
try:
|
| 296 |
+
ref = get_db_ref(f"entries/{entry_id}")
|
| 297 |
+
existing = ref.get()
|
| 298 |
+
if not existing:
|
| 299 |
+
raise HTTPException(status_code=404, detail="Entry not found")
|
| 300 |
+
|
| 301 |
+
updated = {
|
| 302 |
+
**existing,
|
| 303 |
+
"entryType": payload.entryType,
|
| 304 |
+
"category": payload.category,
|
| 305 |
+
"amount": payload.amount,
|
| 306 |
+
"currency": payload.currency or "PKR",
|
| 307 |
+
"paymentMethod": payload.paymentMethod or "cash",
|
| 308 |
+
"notes": payload.notes,
|
| 309 |
+
"updatedAt": datetime.utcnow().isoformat(),
|
| 310 |
+
"meta": payload.meta.model_dump() if payload.meta else existing.get("meta"),
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
ref.set(updated)
|
| 314 |
+
print(f"โ
Entry updated: {entry_id}")
|
| 315 |
+
return {"id": entry_id, **updated}
|
| 316 |
+
except HTTPException:
|
| 317 |
+
raise
|
| 318 |
+
except Exception as e:
|
| 319 |
+
raise HTTPException(status_code=500, detail=f"Firebase error: {str(e)}")
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
@app.delete("/entries/{entry_id}", tags=["entries"])
|
| 323 |
+
async def delete_entry(entry_id: str):
|
| 324 |
+
"""Delete an entry"""
|
| 325 |
+
try:
|
| 326 |
+
ref = get_db_ref(f"entries/{entry_id}")
|
| 327 |
+
if not ref.get():
|
| 328 |
+
raise HTTPException(status_code=404, detail="Entry not found")
|
| 329 |
+
ref.delete()
|
| 330 |
+
print(f"๐๏ธ Entry deleted: {entry_id}")
|
| 331 |
+
return {"message": "Entry deleted successfully", "id": entry_id}
|
| 332 |
+
except HTTPException:
|
| 333 |
+
raise
|
| 334 |
+
except Exception as e:
|
| 335 |
+
raise HTTPException(status_code=500, detail=f"Firebase error: {str(e)}")
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
@app.get("/entries/stats/summary", tags=["entries"])
|
| 339 |
+
async def get_entries_summary():
|
| 340 |
+
"""Get summary statistics for entries"""
|
| 341 |
+
try:
|
| 342 |
+
ref = get_db_ref("entries")
|
| 343 |
+
data = ref.get() or {}
|
| 344 |
+
entries = list(data.values())
|
| 345 |
+
|
| 346 |
+
total_income = sum(e["amount"] for e in entries if e.get("entryType") == "income")
|
| 347 |
+
total_expense = sum(e["amount"] for e in entries if e.get("entryType") == "expense")
|
| 348 |
+
net_balance = total_income - total_expense
|
| 349 |
+
|
| 350 |
+
# Category breakdown
|
| 351 |
+
categories = {}
|
| 352 |
+
for entry in entries:
|
| 353 |
+
cat = entry.get("category", "uncategorized")
|
| 354 |
+
if cat not in categories:
|
| 355 |
+
categories[cat] = {"income": 0, "expense": 0, "count": 0}
|
| 356 |
+
|
| 357 |
+
if entry.get("entryType") == "income":
|
| 358 |
+
categories[cat]["income"] += entry["amount"]
|
| 359 |
+
else:
|
| 360 |
+
categories[cat]["expense"] += entry["amount"]
|
| 361 |
+
categories[cat]["count"] += 1
|
| 362 |
+
|
| 363 |
+
return {
|
| 364 |
+
"total_entries": len(entries),
|
| 365 |
+
"total_income": round(total_income, 2),
|
| 366 |
+
"total_expense": round(total_expense, 2),
|
| 367 |
+
"net_balance": round(net_balance, 2),
|
| 368 |
+
"categories": categories
|
| 369 |
+
}
|
| 370 |
+
except Exception as e:
|
| 371 |
+
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
|
| 372 |
+
|
| 373 |
+
|
| 374 |
+
# ================== MARKET ADVISORY ENDPOINTS ==================
|
| 375 |
+
|
| 376 |
+
@app.get("/advice/{crop}", response_model=MarketAdviceResponse, tags=["market"])
|
| 377 |
+
async def get_crop_advice(
|
| 378 |
+
crop: str,
|
| 379 |
+
risk_tolerance: Optional[str] = Query(None, description="low, medium, or high"),
|
| 380 |
+
budget: Optional[float] = Query(None, description="Available budget in PKR"),
|
| 381 |
+
land_size: Optional[float] = Query(None, description="Land size in acres"),
|
| 382 |
+
):
|
| 383 |
+
"""Get detailed market advice for a specific crop"""
|
| 384 |
+
try:
|
| 385 |
+
farmer_context = {}
|
| 386 |
+
if budget:
|
| 387 |
+
farmer_context["budget"] = budget
|
| 388 |
+
if land_size:
|
| 389 |
+
farmer_context["land_size"] = land_size
|
| 390 |
+
if risk_tolerance:
|
| 391 |
+
farmer_context["risk_tolerance"] = risk_tolerance
|
| 392 |
+
|
| 393 |
+
raw_advice = await get_market_advice(crop.lower(), farmer_context or None)
|
| 394 |
+
structured_response = parse_advice_to_structured(raw_advice, crop)
|
| 395 |
+
|
| 396 |
+
return structured_response
|
| 397 |
+
|
| 398 |
+
except Exception as e:
|
| 399 |
+
print(f"โ Market advice error: {str(e)}")
|
| 400 |
+
raise HTTPException(status_code=500, detail=f"Market agent error: {str(e)}")
|
| 401 |
+
|
| 402 |
+
|
| 403 |
+
@app.post("/advice", response_model=MarketAdviceResponse, tags=["market"])
|
| 404 |
+
async def get_crop_advice_post(request: AdviceRequest):
|
| 405 |
+
"""Get detailed market advice (POST version)"""
|
| 406 |
+
try:
|
| 407 |
+
farmer_context = {}
|
| 408 |
+
if request.budget:
|
| 409 |
+
farmer_context["budget"] = request.budget
|
| 410 |
+
if request.land_size:
|
| 411 |
+
farmer_context["land_size"] = request.land_size
|
| 412 |
+
if request.risk_tolerance:
|
| 413 |
+
farmer_context["risk_tolerance"] = request.risk_tolerance
|
| 414 |
+
|
| 415 |
+
raw_advice = await get_market_advice(request.crop.lower(), farmer_context or None)
|
| 416 |
+
structured_response = parse_advice_to_structured(raw_advice, request.crop)
|
| 417 |
+
|
| 418 |
+
return structured_response
|
| 419 |
+
|
| 420 |
+
except Exception as e:
|
| 421 |
+
print(f"โ Market advice error: {str(e)}")
|
| 422 |
+
raise HTTPException(status_code=500, detail=f"Market agent error: {str(e)}")
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
@app.get("/advice/summary/all", response_model=MarketSummaryResponse, tags=["market"])
|
| 426 |
+
async def get_market_summary(
|
| 427 |
+
risk_tolerance: Optional[str] = Query(None),
|
| 428 |
+
budget: Optional[float] = Query(None),
|
| 429 |
+
land_size: Optional[float] = Query(None),
|
| 430 |
+
):
|
| 431 |
+
"""Get comprehensive market summary for all major crops"""
|
| 432 |
+
try:
|
| 433 |
+
farmer_context = {}
|
| 434 |
+
if budget:
|
| 435 |
+
farmer_context["budget"] = budget
|
| 436 |
+
if land_size:
|
| 437 |
+
farmer_context["land_size"] = land_size
|
| 438 |
+
if risk_tolerance:
|
| 439 |
+
farmer_context["risk_tolerance"] = risk_tolerance
|
| 440 |
+
|
| 441 |
+
crops = ["wheat", "rice", "maize", "cotton", "sugarcane", "potato", "onion", "tomato"]
|
| 442 |
+
crop_summaries: List[CropSummary] = []
|
| 443 |
+
|
| 444 |
+
sell_count = 0
|
| 445 |
+
buy_count = 0
|
| 446 |
+
hold_count = 0
|
| 447 |
+
|
| 448 |
+
for crop in crops:
|
| 449 |
+
try:
|
| 450 |
+
raw_advice = await get_market_advice(crop, farmer_context or None)
|
| 451 |
+
structured = parse_advice_to_structured(raw_advice, crop)
|
| 452 |
+
|
| 453 |
+
summary = CropSummary(
|
| 454 |
+
crop=crop,
|
| 455 |
+
action=structured.action,
|
| 456 |
+
current_price=structured.current_price,
|
| 457 |
+
trend=structured.price_trend,
|
| 458 |
+
confidence=structured.confidence,
|
| 459 |
+
quick_summary=structured.reasoning[:100] + "..." if len(structured.reasoning) > 100 else structured.reasoning
|
| 460 |
+
)
|
| 461 |
+
|
| 462 |
+
crop_summaries.append(summary)
|
| 463 |
+
|
| 464 |
+
if "SELL" in structured.action.upper():
|
| 465 |
+
sell_count += 1
|
| 466 |
+
elif "BUY" in structured.action.upper():
|
| 467 |
+
buy_count += 1
|
| 468 |
+
else:
|
| 469 |
+
hold_count += 1
|
| 470 |
+
|
| 471 |
+
except Exception as crop_error:
|
| 472 |
+
print(f"โ ๏ธ Error for {crop}: {str(crop_error)}")
|
| 473 |
+
crop_summaries.append(CropSummary(
|
| 474 |
+
crop=crop,
|
| 475 |
+
action="ERROR",
|
| 476 |
+
current_price=0.0,
|
| 477 |
+
trend="unavailable",
|
| 478 |
+
confidence="NONE",
|
| 479 |
+
quick_summary=f"Unable to fetch data"
|
| 480 |
+
))
|
| 481 |
+
|
| 482 |
+
market_overview = f"Market Summary: {sell_count} crops SELL, {buy_count} BUY, {hold_count} HOLD. "
|
| 483 |
+
|
| 484 |
+
if sell_count > buy_count:
|
| 485 |
+
market_overview += "Market favoring sellers."
|
| 486 |
+
elif buy_count > sell_count:
|
| 487 |
+
market_overview += "Good time for input purchasing."
|
| 488 |
+
else:
|
| 489 |
+
market_overview += "Mixed market conditions."
|
| 490 |
+
|
| 491 |
+
return MarketSummaryResponse(
|
| 492 |
+
timestamp=datetime.utcnow().isoformat(),
|
| 493 |
+
farmer_context=farmer_context or None,
|
| 494 |
+
crops=crop_summaries,
|
| 495 |
+
market_overview=market_overview
|
| 496 |
+
)
|
| 497 |
+
|
| 498 |
+
except Exception as e:
|
| 499 |
+
print(f"โ Summary error: {str(e)}")
|
| 500 |
+
raise HTTPException(status_code=500, detail=f"Summary error: {str(e)}")
|
| 501 |
+
|
| 502 |
+
|
| 503 |
+
@app.get("/advice/{crop}/raw", tags=["market"])
|
| 504 |
+
async def get_crop_advice_raw(
|
| 505 |
+
crop: str,
|
| 506 |
+
risk_tolerance: Optional[str] = Query(None),
|
| 507 |
+
budget: Optional[float] = Query(None),
|
| 508 |
+
land_size: Optional[float] = Query(None),
|
| 509 |
+
):
|
| 510 |
+
"""Get raw unstructured advice (backward compatibility)"""
|
| 511 |
+
try:
|
| 512 |
+
farmer_context = {}
|
| 513 |
+
if budget:
|
| 514 |
+
farmer_context["budget"] = budget
|
| 515 |
+
if land_size:
|
| 516 |
+
farmer_context["land_size"] = land_size
|
| 517 |
+
if risk_tolerance:
|
| 518 |
+
farmer_context["risk_tolerance"] = risk_tolerance
|
| 519 |
+
|
| 520 |
+
advice = await get_market_advice(crop.lower(), farmer_context or None)
|
| 521 |
+
|
| 522 |
+
return {
|
| 523 |
+
"crop": crop,
|
| 524 |
+
"advice": advice,
|
| 525 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 526 |
+
}
|
| 527 |
+
except Exception as e:
|
| 528 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 529 |
+
|
| 530 |
+
|
| 531 |
+
# ================== MARKET DATA ENDPOINTS ==================
|
| 532 |
+
|
| 533 |
+
@app.get("/market/prices/current", tags=["market-data"])
|
| 534 |
+
async def get_current_prices(crop: Optional[str] = Query(None)):
|
| 535 |
+
"""Get current crop prices from seeded data"""
|
| 536 |
+
try:
|
| 537 |
+
ref = get_db_ref("market_data/latest")
|
| 538 |
+
data = ref.get()
|
| 539 |
+
|
| 540 |
+
if not data:
|
| 541 |
+
raise HTTPException(status_code=404, detail="No market data. Please seed database first.")
|
| 542 |
+
|
| 543 |
+
if crop:
|
| 544 |
+
price = data.get("prices", {}).get(crop)
|
| 545 |
+
if not price:
|
| 546 |
+
raise HTTPException(status_code=404, detail=f"No price data for {crop}")
|
| 547 |
+
return {
|
| 548 |
+
"crop": crop,
|
| 549 |
+
"price": price,
|
| 550 |
+
"unit": "PKR per 40kg",
|
| 551 |
+
"timestamp": data.get("timestamp")
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
return data
|
| 555 |
+
except HTTPException:
|
| 556 |
+
raise
|
| 557 |
+
except Exception as e:
|
| 558 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 559 |
+
|
| 560 |
+
|
| 561 |
+
@app.get("/market/prices/history/{crop}", tags=["market-data"])
|
| 562 |
+
async def get_crop_price_history(
|
| 563 |
+
crop: str,
|
| 564 |
+
days: int = Query(30, ge=1, le=90)
|
| 565 |
+
):
|
| 566 |
+
"""Get price history for a specific crop"""
|
| 567 |
+
try:
|
| 568 |
+
from ageents.marketAgent import get_historical_prices
|
| 569 |
+
|
| 570 |
+
history = await get_historical_prices(crop, days)
|
| 571 |
+
|
| 572 |
+
if history.get("error"):
|
| 573 |
+
raise HTTPException(status_code=404, detail=history["error"])
|
| 574 |
+
|
| 575 |
+
return {
|
| 576 |
+
"crop": crop,
|
| 577 |
+
"period_days": days,
|
| 578 |
+
"data_points": history.get("data_points", []),
|
| 579 |
+
"total_records": history.get("total_records", 0)
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
except HTTPException:
|
| 583 |
+
raise
|
| 584 |
+
except Exception as e:
|
| 585 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 586 |
+
|
| 587 |
+
|
| 588 |
+
@app.get("/market/analytics/{crop}", tags=["market-data"])
|
| 589 |
+
async def get_crop_analytics(
|
| 590 |
+
crop: str,
|
| 591 |
+
days: int = Query(7, ge=1, le=90)
|
| 592 |
+
):
|
| 593 |
+
"""Get analytical insights for a crop"""
|
| 594 |
+
try:
|
| 595 |
+
from ageents.marketAgent import calculate_price_trend
|
| 596 |
+
|
| 597 |
+
trend_data = await calculate_price_trend(crop, days)
|
| 598 |
+
|
| 599 |
+
if trend_data.get("error"):
|
| 600 |
+
raise HTTPException(status_code=404, detail=trend_data["error"])
|
| 601 |
+
|
| 602 |
+
return trend_data
|
| 603 |
+
|
| 604 |
+
except HTTPException:
|
| 605 |
+
raise
|
| 606 |
+
except Exception as e:
|
| 607 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 608 |
+
|
| 609 |
+
|
| 610 |
+
@app.get("/market/crops", tags=["market-data"])
|
| 611 |
+
async def list_available_crops():
|
| 612 |
+
"""List all available crops with data"""
|
| 613 |
+
try:
|
| 614 |
+
ref = get_db_ref("market_data/latest")
|
| 615 |
+
data = ref.get()
|
| 616 |
+
|
| 617 |
+
if not data or "prices" not in data:
|
| 618 |
+
return {"crops": [], "message": "No data available. Please seed database."}
|
| 619 |
+
|
| 620 |
+
crops = []
|
| 621 |
+
for crop, price in data["prices"].items():
|
| 622 |
+
crops.append({
|
| 623 |
+
"name": crop,
|
| 624 |
+
"current_price": price,
|
| 625 |
+
"unit": "PKR per 40kg"
|
| 626 |
+
})
|
| 627 |
+
|
| 628 |
+
return {"crops": crops, "total": len(crops)}
|
| 629 |
+
except Exception as e:
|
| 630 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 631 |
+
|
| 632 |
+
# ---------------- WEATHER + SOIL ENDPOINTS ----------------
|
| 633 |
+
from ageents.weatherAgent import run_weather_agent, fetch_current, fetch_forecast
|
| 634 |
+
from ageents.soilAgent import run_soil_agent
|
| 635 |
+
from ageents.predictionAgent import run_prediction_workflow
|
| 636 |
+
from soil_data_seeder import seed_200_soil_entries, verify_soil_data # import soil functions
|
| 637 |
+
from fastapi import Query
|
| 638 |
+
from pydantic import BaseModel
|
| 639 |
+
|
| 640 |
+
|
| 641 |
+
# ---------------- Request/Response Models ----------------
|
| 642 |
+
class PredictionRequest(BaseModel):
|
| 643 |
+
location: str
|
| 644 |
+
crop: str
|
| 645 |
+
city: Optional[str] = None
|
| 646 |
+
|
| 647 |
+
class MultiLocationRequest(BaseModel):
|
| 648 |
+
locations: List[str]
|
| 649 |
+
crop: Optional[str] = None
|
| 650 |
+
|
| 651 |
+
# ============================================================
|
| 652 |
+
# WEATHER ENDPOINTS
|
| 653 |
+
# ============================================================
|
| 654 |
+
|
| 655 |
+
@app.get("/weather", tags=["weather"])
|
| 656 |
+
async def get_weather(city: str = Query(..., description="City name for weather data")):
|
| 657 |
+
"""Get current and forecast weather for a given city with AI analysis"""
|
| 658 |
+
try:
|
| 659 |
+
result = await run_weather_agent(city)
|
| 660 |
+
return result
|
| 661 |
+
except Exception as e:
|
| 662 |
+
raise HTTPException(status_code=500, detail=f"Weather error: {str(e)}")
|
| 663 |
+
|
| 664 |
+
@app.get("/weather/summary", tags=["weather"])
|
| 665 |
+
async def weather_summary(city: str = Query(..., description="City name for summary")):
|
| 666 |
+
"""Short summary of weather trend and recommendations"""
|
| 667 |
+
try:
|
| 668 |
+
result = await run_weather_agent(city)
|
| 669 |
+
summary = {
|
| 670 |
+
"city": result.get("city"),
|
| 671 |
+
"trend": result.get("trend"),
|
| 672 |
+
"precipitation_chance": result.get("precipitation_chance"),
|
| 673 |
+
"recommendations": result.get("recommendations"),
|
| 674 |
+
}
|
| 675 |
+
return summary
|
| 676 |
+
except Exception as e:
|
| 677 |
+
raise HTTPException(status_code=500, detail=f"Weather summary error: {str(e)}")
|
| 678 |
+
|
| 679 |
+
@app.get("/weather/raw", tags=["weather"])
|
| 680 |
+
def get_raw_weather(city: str = Query(...)):
|
| 681 |
+
"""Raw OpenWeatherMap current + forecast (no AI processing)"""
|
| 682 |
+
try:
|
| 683 |
+
current = fetch_current(city)
|
| 684 |
+
forecast = fetch_forecast(city)
|
| 685 |
+
return {"current": current, "forecast": forecast}
|
| 686 |
+
except Exception as e:
|
| 687 |
+
raise HTTPException(status_code=500, detail=f"Raw weather error: {str(e)}")
|
| 688 |
+
|
| 689 |
+
@app.get("/weather/forecast", tags=["weather"])
|
| 690 |
+
async def get_weather_forecast(
|
| 691 |
+
city: str = Query(..., description="City name for forecast"),
|
| 692 |
+
days: int = Query(3, ge=1, le=7, description="Number of days for forecast (1โ7)")
|
| 693 |
+
):
|
| 694 |
+
"""
|
| 695 |
+
Get detailed weather forecast every 3 hours for the next N days.
|
| 696 |
+
Uses OpenWeatherMap 3-hour forecast data.
|
| 697 |
+
"""
|
| 698 |
+
try:
|
| 699 |
+
forecast_data = fetch_forecast(city)
|
| 700 |
+
|
| 701 |
+
if not forecast_data or "list" not in forecast_data:
|
| 702 |
+
raise HTTPException(status_code=404, detail="Forecast data unavailable for this city")
|
| 703 |
+
|
| 704 |
+
filtered_forecast = []
|
| 705 |
+
for item in forecast_data["list"]:
|
| 706 |
+
timestamp = item["dt_txt"]
|
| 707 |
+
temp = item["main"]["temp"]
|
| 708 |
+
humidity = item["main"]["humidity"]
|
| 709 |
+
weather_desc = item["weather"][0]["description"]
|
| 710 |
+
wind_speed = item["wind"]["speed"]
|
| 711 |
+
|
| 712 |
+
filtered_forecast.append({
|
| 713 |
+
"time": timestamp,
|
| 714 |
+
"temperature": round(temp, 1),
|
| 715 |
+
"humidity": humidity,
|
| 716 |
+
"description": weather_desc.capitalize(),
|
| 717 |
+
"wind_speed": wind_speed
|
| 718 |
+
})
|
| 719 |
+
|
| 720 |
+
# Limit to N days (8 entries per day ร days)
|
| 721 |
+
limit = days * 8
|
| 722 |
+
return {
|
| 723 |
+
"city": city,
|
| 724 |
+
"forecast_hours": len(filtered_forecast[:limit]),
|
| 725 |
+
"data": filtered_forecast[:limit],
|
| 726 |
+
"unit": "Celsius",
|
| 727 |
+
"source": "OpenWeatherMap"
|
| 728 |
+
}
|
| 729 |
+
|
| 730 |
+
except HTTPException:
|
| 731 |
+
raise
|
| 732 |
+
except Exception as e:
|
| 733 |
+
raise HTTPException(status_code=500, detail=f"Forecast error: {str(e)}")
|
| 734 |
+
|
| 735 |
+
@app.post("/weather/multi", tags=["weather"])
|
| 736 |
+
async def get_multiple_weather(cities: List[str]):
|
| 737 |
+
"""Batch weather insights for multiple cities"""
|
| 738 |
+
results = {}
|
| 739 |
+
for city in cities:
|
| 740 |
+
try:
|
| 741 |
+
results[city] = await run_weather_agent(city)
|
| 742 |
+
except Exception as e:
|
| 743 |
+
results[city] = {"error": str(e)}
|
| 744 |
+
return results
|
| 745 |
+
|
| 746 |
+
# ============================================================
|
| 747 |
+
# SOIL ENDPOINTS
|
| 748 |
+
# ============================================================
|
| 749 |
+
|
| 750 |
+
@app.get("/soil", tags=["soil"])
|
| 751 |
+
async def get_soil_analysis(
|
| 752 |
+
location: str = Query(..., description="Location name for soil data"),
|
| 753 |
+
crop: Optional[str] = Query(None, description="Optional: specific crop for analysis")
|
| 754 |
+
):
|
| 755 |
+
"""Get comprehensive soil analysis for a location"""
|
| 756 |
+
try:
|
| 757 |
+
result = await run_soil_agent(location, crop)
|
| 758 |
+
return result
|
| 759 |
+
except Exception as e:
|
| 760 |
+
raise HTTPException(status_code=500, detail=f"Soil analysis error: {str(e)}")
|
| 761 |
+
|
| 762 |
+
@app.get("/soil/summary", tags=["soil"])
|
| 763 |
+
async def get_soil_summary(
|
| 764 |
+
location: str = Query(..., description="Location name"),
|
| 765 |
+
crop: Optional[str] = Query(None, description="Optional: crop name")
|
| 766 |
+
):
|
| 767 |
+
"""Short summary of soil health and recommendations"""
|
| 768 |
+
try:
|
| 769 |
+
result = await run_soil_agent(location, crop)
|
| 770 |
+
summary = {
|
| 771 |
+
"location": result.get("location"),
|
| 772 |
+
"crop": result.get("crop"),
|
| 773 |
+
"soil_health_score": result.get("soil_health_score"),
|
| 774 |
+
"nutrient_status": result.get("nutrient_status"),
|
| 775 |
+
"warnings": result.get("warnings"),
|
| 776 |
+
"recommendations": result.get("recommendations")[:3] # Top 3
|
| 777 |
+
}
|
| 778 |
+
return summary
|
| 779 |
+
except Exception as e:
|
| 780 |
+
raise HTTPException(status_code=500, detail=f"Soil summary error: {str(e)}")
|
| 781 |
+
|
| 782 |
+
@app.get("/soil/nutrients", tags=["soil"])
|
| 783 |
+
async def get_nutrient_status(
|
| 784 |
+
location: str = Query(..., description="Location name"),
|
| 785 |
+
crop: Optional[str] = Query(None, description="Optional: crop name")
|
| 786 |
+
):
|
| 787 |
+
"""Get detailed nutrient status and fertilizer recommendations"""
|
| 788 |
+
try:
|
| 789 |
+
result = await run_soil_agent(location, crop)
|
| 790 |
+
return {
|
| 791 |
+
"location": result.get("location"),
|
| 792 |
+
"nutrient_status": result.get("nutrient_status"),
|
| 793 |
+
"fertilizer_plan": result.get("fertilizer_plan"),
|
| 794 |
+
"deficiencies": result.get("deficiencies"),
|
| 795 |
+
"soil_health_score": result.get("soil_health_score")
|
| 796 |
+
}
|
| 797 |
+
except Exception as e:
|
| 798 |
+
raise HTTPException(status_code=500, detail=f"Nutrient analysis error: {str(e)}")
|
| 799 |
+
|
| 800 |
+
@app.get("/soil/irrigation", tags=["soil"])
|
| 801 |
+
async def get_irrigation_advice(
|
| 802 |
+
location: str = Query(..., description="Location name"),
|
| 803 |
+
crop: Optional[str] = Query(None, description="Optional: crop name")
|
| 804 |
+
):
|
| 805 |
+
"""Get irrigation recommendations based on soil moisture"""
|
| 806 |
+
try:
|
| 807 |
+
result = await run_soil_agent(location, crop)
|
| 808 |
+
soil_data = result.get("soil_data", {})
|
| 809 |
+
return {
|
| 810 |
+
"location": result.get("location"),
|
| 811 |
+
"irrigation_advice": result.get("irrigation_advice"),
|
| 812 |
+
"current_moisture": soil_data.get("moisture_percent"),
|
| 813 |
+
"optimal_range": "15-25%",
|
| 814 |
+
"recommendations": [r for r in result.get("recommendations", []) if "irrigation" in r.lower()]
|
| 815 |
+
}
|
| 816 |
+
except Exception as e:
|
| 817 |
+
raise HTTPException(status_code=500, detail=f"Irrigation advice error: {str(e)}")
|
| 818 |
+
|
| 819 |
+
@app.post("/soil/multi", tags=["soil"])
|
| 820 |
+
async def get_multiple_soil_analysis(request: MultiLocationRequest):
|
| 821 |
+
"""Batch soil analysis for multiple locations"""
|
| 822 |
+
results = {}
|
| 823 |
+
for location in request.locations:
|
| 824 |
+
try:
|
| 825 |
+
results[location] = await run_soil_agent(location, request.crop)
|
| 826 |
+
except Exception as e:
|
| 827 |
+
results[location] = {"error": str(e)}
|
| 828 |
+
return results
|
| 829 |
+
|
| 830 |
+
@app.post("/seed-soil", tags=["soil"])
|
| 831 |
+
async def seed_soil():
|
| 832 |
+
"""Seed 200 soil data entries into Firebase"""
|
| 833 |
+
try:
|
| 834 |
+
entries = seed_200_soil_entries()
|
| 835 |
+
return {
|
| 836 |
+
"status": "success",
|
| 837 |
+
"entries_created": len(entries),
|
| 838 |
+
"message": "Soil database seeded successfully"
|
| 839 |
+
}
|
| 840 |
+
except Exception as e:
|
| 841 |
+
raise HTTPException(status_code=500, detail=f"Soil seeding error: {str(e)}")
|
| 842 |
+
|
| 843 |
+
@app.get("/verify-soil", tags=["soil"])
|
| 844 |
+
async def verify_soil():
|
| 845 |
+
"""Verify soil data integrity in Firebase"""
|
| 846 |
+
try:
|
| 847 |
+
verify_soil_data()
|
| 848 |
+
return {"status": "verification complete", "message": "Soil data verified successfully"}
|
| 849 |
+
except Exception as e:
|
| 850 |
+
raise HTTPException(status_code=500, detail=f"Soil verification error: {str(e)}")
|
| 851 |
+
|
| 852 |
+
# ============================================================
|
| 853 |
+
# PREDICTION WORKFLOW ENDPOINTS
|
| 854 |
+
# ============================================================
|
| 855 |
+
|
| 856 |
+
@app.post("/prediction", tags=["prediction"])
|
| 857 |
+
async def get_farm_prediction(request: PredictionRequest):
|
| 858 |
+
"""
|
| 859 |
+
Get comprehensive farm prediction by combining weather and soil analysis.
|
| 860 |
+
This is the main workflow endpoint that orchestrates multiple agents.
|
| 861 |
+
"""
|
| 862 |
+
try:
|
| 863 |
+
result = await run_prediction_workflow(
|
| 864 |
+
location=request.location,
|
| 865 |
+
crop=request.crop,
|
| 866 |
+
city=request.city
|
| 867 |
+
)
|
| 868 |
+
return result
|
| 869 |
+
except Exception as e:
|
| 870 |
+
raise HTTPException(status_code=500, detail=f"Prediction workflow error: {str(e)}")
|
| 871 |
+
|
| 872 |
+
@app.get("/prediction/quick", tags=["prediction"])
|
| 873 |
+
async def get_quick_prediction(
|
| 874 |
+
location: str = Query(..., description="Farm location"),
|
| 875 |
+
crop: str = Query(..., description="Crop name"),
|
| 876 |
+
city: Optional[str] = Query(None, description="City for weather (optional)")
|
| 877 |
+
):
|
| 878 |
+
"""Quick prediction summary (faster response, less detail)"""
|
| 879 |
+
try:
|
| 880 |
+
result = await run_prediction_workflow(location, crop, city)
|
| 881 |
+
|
| 882 |
+
# Return only essential information
|
| 883 |
+
quick_summary = {
|
| 884 |
+
"location": result.get("location"),
|
| 885 |
+
"crop": result.get("crop"),
|
| 886 |
+
"risk_level": result.get("risk_level"),
|
| 887 |
+
"success_probability": result.get("success_probability"),
|
| 888 |
+
"immediate_actions": result.get("immediate_actions", [])[:3],
|
| 889 |
+
"executive_summary": result.get("executive_summary")
|
| 890 |
+
}
|
| 891 |
+
return quick_summary
|
| 892 |
+
except Exception as e:
|
| 893 |
+
raise HTTPException(status_code=500, detail=f"Quick prediction error: {str(e)}")
|
| 894 |
+
|
| 895 |
+
@app.get("/prediction/detailed", tags=["prediction"])
|
| 896 |
+
async def get_detailed_prediction(
|
| 897 |
+
location: str = Query(..., description="Farm location"),
|
| 898 |
+
crop: str = Query(..., description="Crop name"),
|
| 899 |
+
city: Optional[str] = Query(None, description="City for weather (optional)")
|
| 900 |
+
):
|
| 901 |
+
"""Detailed prediction with complete weather and soil data"""
|
| 902 |
+
try:
|
| 903 |
+
result = await run_prediction_workflow(location, crop, city)
|
| 904 |
+
return result
|
| 905 |
+
except Exception as e:
|
| 906 |
+
raise HTTPException(status_code=500, detail=f"Detailed prediction error: {str(e)}")
|
| 907 |
+
|
| 908 |
+
@app.get("/prediction/actions", tags=["prediction"])
|
| 909 |
+
async def get_action_plan(
|
| 910 |
+
location: str = Query(..., description="Farm location"),
|
| 911 |
+
crop: str = Query(..., description="Crop name"),
|
| 912 |
+
city: Optional[str] = Query(None, description="City for weather (optional)")
|
| 913 |
+
):
|
| 914 |
+
"""Get time-based action plan (immediate/short-term/long-term)"""
|
| 915 |
+
try:
|
| 916 |
+
result = await run_prediction_workflow(location, crop, city)
|
| 917 |
+
|
| 918 |
+
return {
|
| 919 |
+
"location": result.get("location"),
|
| 920 |
+
"crop": result.get("crop"),
|
| 921 |
+
"risk_level": result.get("risk_level"),
|
| 922 |
+
"immediate_actions": result.get("immediate_actions", []),
|
| 923 |
+
"short_term_actions": result.get("short_term_actions", []),
|
| 924 |
+
"long_term_actions": result.get("long_term_actions", []),
|
| 925 |
+
"irrigation_schedule": result.get("irrigation_schedule"),
|
| 926 |
+
"fertilizer_schedule": result.get("fertilizer_schedule")
|
| 927 |
+
}
|
| 928 |
+
except Exception as e:
|
| 929 |
+
raise HTTPException(status_code=500, detail=f"Action plan error: {str(e)}")
|
| 930 |
+
|
| 931 |
+
@app.get("/prediction/risks", tags=["prediction"])
|
| 932 |
+
async def get_risk_assessment(
|
| 933 |
+
location: str = Query(..., description="Farm location"),
|
| 934 |
+
crop: str = Query(..., description="Crop name"),
|
| 935 |
+
city: Optional[str] = Query(None, description="City for weather (optional)")
|
| 936 |
+
):
|
| 937 |
+
"""Get comprehensive risk assessment"""
|
| 938 |
+
try:
|
| 939 |
+
result = await run_prediction_workflow(location, crop, city)
|
| 940 |
+
|
| 941 |
+
return {
|
| 942 |
+
"location": result.get("location"),
|
| 943 |
+
"crop": result.get("crop"),
|
| 944 |
+
"risk_level": result.get("risk_level"),
|
| 945 |
+
"success_probability": result.get("success_probability"),
|
| 946 |
+
"weather_risks": result.get("weather_risks", []),
|
| 947 |
+
"soil_risks": result.get("soil_risks", []),
|
| 948 |
+
"pest_disease_risk": result.get("pest_disease_risk"),
|
| 949 |
+
"mitigation_actions": result.get("immediate_actions", [])
|
| 950 |
+
}
|
| 951 |
+
except Exception as e:
|
| 952 |
+
raise HTTPException(status_code=500, detail=f"Risk assessment error: {str(e)}")
|
| 953 |
+
|
| 954 |
+
@app.post("/prediction/batch", tags=["prediction"])
|
| 955 |
+
async def batch_predictions(locations: List[str], crop: str):
|
| 956 |
+
"""Run predictions for multiple locations with the same crop"""
|
| 957 |
+
results = {}
|
| 958 |
+
for location in locations:
|
| 959 |
+
try:
|
| 960 |
+
result = await run_prediction_workflow(location, crop)
|
| 961 |
+
results[location] = {
|
| 962 |
+
"risk_level": result.get("risk_level"),
|
| 963 |
+
"success_probability": result.get("success_probability"),
|
| 964 |
+
"executive_summary": result.get("executive_summary")
|
| 965 |
+
}
|
| 966 |
+
except Exception as e:
|
| 967 |
+
results[location] = {"error": str(e)}
|
| 968 |
+
return results
|
| 969 |
+
|
| 970 |
+
# ============================================================
|
| 971 |
+
# INTEGRATED ENDPOINTS (Weather + Soil Combined)
|
| 972 |
+
# ============================================================
|
| 973 |
+
|
| 974 |
+
@app.get("/integrated/overview", tags=["integrated"])
|
| 975 |
+
async def get_integrated_overview(
|
| 976 |
+
location: str = Query(..., description="Location name"),
|
| 977 |
+
crop: str = Query(..., description="Crop name"),
|
| 978 |
+
city: Optional[str] = Query(None, description="City for weather")
|
| 979 |
+
):
|
| 980 |
+
"""Get side-by-side weather and soil overview (without prediction synthesis)"""
|
| 981 |
+
try:
|
| 982 |
+
weather_city = city or location
|
| 983 |
+
|
| 984 |
+
# Fetch both in parallel
|
| 985 |
+
import asyncio
|
| 986 |
+
weather_task = run_weather_agent(weather_city)
|
| 987 |
+
soil_task = run_soil_agent(location, crop)
|
| 988 |
+
|
| 989 |
+
weather_result, soil_result = await asyncio.gather(weather_task, soil_task)
|
| 990 |
+
|
| 991 |
+
return {
|
| 992 |
+
"location": location,
|
| 993 |
+
"crop": crop,
|
| 994 |
+
"weather": {
|
| 995 |
+
"city": weather_result.get("city"),
|
| 996 |
+
"trend": weather_result.get("trend"),
|
| 997 |
+
"precipitation_chance": weather_result.get("precipitation_chance"),
|
| 998 |
+
"recommendations": weather_result.get("recommendations")
|
| 999 |
+
},
|
| 1000 |
+
"soil": {
|
| 1001 |
+
"health_score": soil_result.get("soil_health_score"),
|
| 1002 |
+
"nutrient_status": soil_result.get("nutrient_status"),
|
| 1003 |
+
"warnings": soil_result.get("warnings"),
|
| 1004 |
+
"recommendations": soil_result.get("recommendations")
|
| 1005 |
+
}
|
| 1006 |
+
}
|
| 1007 |
+
except Exception as e:
|
| 1008 |
+
raise HTTPException(status_code=500, detail=f"Integrated overview error: {str(e)}")
|
| 1009 |
+
|
| 1010 |
+
# ---------------- ADVISORY ENDPOINTS ----------------
|
| 1011 |
+
# Add these to your main FastAPI app file (after the prediction endpoints)
|
| 1012 |
+
|
| 1013 |
+
from ageents.advisoryAgent import run_advisory_agent
|
| 1014 |
+
from pydantic import BaseModel
|
| 1015 |
+
|
| 1016 |
+
class AdvisoryRequest(BaseModel):
|
| 1017 |
+
location: str
|
| 1018 |
+
crop: str
|
| 1019 |
+
city: Optional[str] = None
|
| 1020 |
+
language: Optional[str] = "english" # Future: support multiple languages
|
| 1021 |
+
|
| 1022 |
+
# ============================================================
|
| 1023 |
+
# ADVISORY ENDPOINTS
|
| 1024 |
+
# ============================================================
|
| 1025 |
+
|
| 1026 |
+
@app.post("/advisory", tags=["advisory"])
|
| 1027 |
+
async def get_farm_advisory(request: AdvisoryRequest):
|
| 1028 |
+
"""
|
| 1029 |
+
Get comprehensive farmer-friendly advisory.
|
| 1030 |
+
Transforms raw prediction data into actionable, easy-to-understand advice.
|
| 1031 |
+
"""
|
| 1032 |
+
try:
|
| 1033 |
+
result = await run_advisory_agent(
|
| 1034 |
+
location=request.location,
|
| 1035 |
+
crop=request.crop,
|
| 1036 |
+
city=request.city
|
| 1037 |
+
)
|
| 1038 |
+
return result
|
| 1039 |
+
except Exception as e:
|
| 1040 |
+
raise HTTPException(status_code=500, detail=f"Advisory generation error: {str(e)}")
|
| 1041 |
+
|
| 1042 |
+
@app.get("/advisory/quick", tags=["advisory"])
|
| 1043 |
+
async def get_quick_advisory(
|
| 1044 |
+
location: str = Query(..., description="Farm location"),
|
| 1045 |
+
crop: str = Query(..., description="Crop name"),
|
| 1046 |
+
city: Optional[str] = Query(None, description="City for weather")
|
| 1047 |
+
):
|
| 1048 |
+
"""
|
| 1049 |
+
Quick advisory summary - just the essentials for busy farmers.
|
| 1050 |
+
Returns: status, urgent actions, and key recommendations.
|
| 1051 |
+
"""
|
| 1052 |
+
try:
|
| 1053 |
+
result = await run_advisory_agent(location, crop, city)
|
| 1054 |
+
|
| 1055 |
+
quick_summary = {
|
| 1056 |
+
"status_emoji": result.get("status_emoji"),
|
| 1057 |
+
"status_message": result.get("status_message"),
|
| 1058 |
+
"situation_summary": result.get("situation_summary"),
|
| 1059 |
+
"urgent_today": result.get("urgent_today", []),
|
| 1060 |
+
"irrigation_guidance": result.get("irrigation_guidance"),
|
| 1061 |
+
"pest_risk_level": result.get("pest_risk_level"),
|
| 1062 |
+
"key_reminders": result.get("key_reminders", [])
|
| 1063 |
+
}
|
| 1064 |
+
return quick_summary
|
| 1065 |
+
except Exception as e:
|
| 1066 |
+
raise HTTPException(status_code=500, detail=f"Quick advisory error: {str(e)}")
|
| 1067 |
+
|
| 1068 |
+
@app.get("/advisory/actions", tags=["advisory"])
|
| 1069 |
+
async def get_action_checklist(
|
| 1070 |
+
location: str = Query(..., description="Farm location"),
|
| 1071 |
+
crop: str = Query(..., description="Crop name"),
|
| 1072 |
+
city: Optional[str] = Query(None, description="City for weather")
|
| 1073 |
+
):
|
| 1074 |
+
"""
|
| 1075 |
+
Get time-based action checklist in simple language.
|
| 1076 |
+
Perfect for farmers who want a clear to-do list.
|
| 1077 |
+
"""
|
| 1078 |
+
try:
|
| 1079 |
+
result = await run_advisory_agent(location, crop, city)
|
| 1080 |
+
|
| 1081 |
+
return {
|
| 1082 |
+
"farm_name": result.get("farm_name"),
|
| 1083 |
+
"advisory_date": result.get("advisory_date"),
|
| 1084 |
+
"status": {
|
| 1085 |
+
"emoji": result.get("status_emoji"),
|
| 1086 |
+
"message": result.get("status_message")
|
| 1087 |
+
},
|
| 1088 |
+
"actions": {
|
| 1089 |
+
"urgent_today": result.get("urgent_today", []),
|
| 1090 |
+
"this_week": result.get("this_week", []),
|
| 1091 |
+
"plan_ahead": result.get("plan_ahead", [])
|
| 1092 |
+
},
|
| 1093 |
+
"next_check": result.get("next_advisory_date")
|
| 1094 |
+
}
|
| 1095 |
+
except Exception as e:
|
| 1096 |
+
raise HTTPException(status_code=500, detail=f"Action checklist error: {str(e)}")
|
| 1097 |
+
|
| 1098 |
+
@app.get("/advisory/weather-tips", tags=["advisory"])
|
| 1099 |
+
async def get_weather_tips(
|
| 1100 |
+
location: str = Query(..., description="Farm location"),
|
| 1101 |
+
crop: str = Query(..., description="Crop name"),
|
| 1102 |
+
city: Optional[str] = Query(None, description="City for weather")
|
| 1103 |
+
):
|
| 1104 |
+
"""
|
| 1105 |
+
Get weather-specific guidance and tips.
|
| 1106 |
+
Explains how weather will affect the crop and what to do.
|
| 1107 |
+
"""
|
| 1108 |
+
try:
|
| 1109 |
+
result = await run_advisory_agent(location, crop, city)
|
| 1110 |
+
|
| 1111 |
+
return {
|
| 1112 |
+
"location": result.get("farm_name"),
|
| 1113 |
+
"crop": crop,
|
| 1114 |
+
"weather_outlook": result.get("weather_outlook"),
|
| 1115 |
+
"weather_impact": result.get("weather_impact"),
|
| 1116 |
+
"weather_tips": result.get("weather_tips", []),
|
| 1117 |
+
"irrigation_guidance": result.get("irrigation_guidance"),
|
| 1118 |
+
"water_saving_tips": result.get("water_saving_tips", [])
|
| 1119 |
+
}
|
| 1120 |
+
except Exception as e:
|
| 1121 |
+
raise HTTPException(status_code=500, detail=f"Weather tips error: {str(e)}")
|
| 1122 |
+
|
| 1123 |
+
@app.get("/advisory/soil-nutrition", tags=["advisory"])
|
| 1124 |
+
async def get_soil_nutrition_advice(
|
| 1125 |
+
location: str = Query(..., description="Farm location"),
|
| 1126 |
+
crop: str = Query(..., description="Crop name"),
|
| 1127 |
+
city: Optional[str] = Query(None, description="City for weather")
|
| 1128 |
+
):
|
| 1129 |
+
"""
|
| 1130 |
+
Get soil health and nutrition advice in simple terms.
|
| 1131 |
+
Tells farmers what their soil needs and how to improve it.
|
| 1132 |
+
"""
|
| 1133 |
+
try:
|
| 1134 |
+
result = await run_advisory_agent(location, crop, city)
|
| 1135 |
+
|
| 1136 |
+
return {
|
| 1137 |
+
"location": result.get("farm_name"),
|
| 1138 |
+
"crop": crop,
|
| 1139 |
+
"soil_health_status": result.get("soil_health_status"),
|
| 1140 |
+
"nutrition_advice": result.get("nutrition_advice"),
|
| 1141 |
+
"soil_tips": result.get("soil_tips", []),
|
| 1142 |
+
"cost_saving_opportunities": result.get("cost_saving_opportunities", [])
|
| 1143 |
+
}
|
| 1144 |
+
except Exception as e:
|
| 1145 |
+
raise HTTPException(status_code=500, detail=f"Soil nutrition advice error: {str(e)}")
|
| 1146 |
+
|
| 1147 |
+
@app.get("/advisory/pest-disease", tags=["advisory"])
|
| 1148 |
+
async def get_pest_disease_advisory(
|
| 1149 |
+
location: str = Query(..., description="Farm location"),
|
| 1150 |
+
crop: str = Query(..., description="Crop name"),
|
| 1151 |
+
city: Optional[str] = Query(None, description="City for weather")
|
| 1152 |
+
):
|
| 1153 |
+
"""
|
| 1154 |
+
Get pest and disease prevention advice.
|
| 1155 |
+
Helps farmers protect their crops proactively.
|
| 1156 |
+
"""
|
| 1157 |
+
try:
|
| 1158 |
+
result = await run_advisory_agent(location, crop, city)
|
| 1159 |
+
|
| 1160 |
+
return {
|
| 1161 |
+
"location": result.get("farm_name"),
|
| 1162 |
+
"crop": crop,
|
| 1163 |
+
"pest_risk_level": result.get("pest_risk_level"),
|
| 1164 |
+
"pest_advice": result.get("pest_advice"),
|
| 1165 |
+
"prevention_steps": result.get("prevention_steps", []),
|
| 1166 |
+
"what_to_watch": "Check crops regularly for unusual spots, holes, or wilting"
|
| 1167 |
+
}
|
| 1168 |
+
except Exception as e:
|
| 1169 |
+
raise HTTPException(status_code=500, detail=f"Pest advisory error: {str(e)}")
|
| 1170 |
+
|
| 1171 |
+
@app.get("/advisory/harvest", tags=["advisory"])
|
| 1172 |
+
async def get_harvest_advisory(
|
| 1173 |
+
location: str = Query(..., description="Farm location"),
|
| 1174 |
+
crop: str = Query(..., description="Crop name"),
|
| 1175 |
+
city: Optional[str] = Query(None, description="City for weather")
|
| 1176 |
+
):
|
| 1177 |
+
"""
|
| 1178 |
+
Get harvest planning and timing advice.
|
| 1179 |
+
Helps farmers know when and how to harvest for best results.
|
| 1180 |
+
"""
|
| 1181 |
+
try:
|
| 1182 |
+
result = await run_advisory_agent(location, crop, city)
|
| 1183 |
+
|
| 1184 |
+
return {
|
| 1185 |
+
"location": result.get("farm_name"),
|
| 1186 |
+
"crop": crop,
|
| 1187 |
+
"harvest_readiness": result.get("harvest_readiness"),
|
| 1188 |
+
"harvest_timing": result.get("harvest_timing"),
|
| 1189 |
+
"harvest_tips": result.get("harvest_tips", []),
|
| 1190 |
+
"yield_expectation": result.get("yield_expectation"),
|
| 1191 |
+
"market_advice": result.get("market_advice")
|
| 1192 |
+
}
|
| 1193 |
+
except Exception as e:
|
| 1194 |
+
raise HTTPException(status_code=500, detail=f"Harvest advisory error: {str(e)}")
|
| 1195 |
+
|
| 1196 |
+
@app.get("/advisory/report", tags=["advisory"])
|
| 1197 |
+
async def get_advisory_report(
|
| 1198 |
+
location: str = Query(..., description="Farm location"),
|
| 1199 |
+
crop: str = Query(..., description="Crop name"),
|
| 1200 |
+
city: Optional[str] = Query(None, description="City for weather")
|
| 1201 |
+
):
|
| 1202 |
+
"""
|
| 1203 |
+
Get complete advisory report formatted for printing or sharing.
|
| 1204 |
+
Perfect for farmers who want a full reference document.
|
| 1205 |
+
"""
|
| 1206 |
+
try:
|
| 1207 |
+
result = await run_advisory_agent(location, crop, city)
|
| 1208 |
+
|
| 1209 |
+
# Format as a readable report
|
| 1210 |
+
report = {
|
| 1211 |
+
"header": {
|
| 1212 |
+
"title": f"Farm Advisory Report - {result.get('farm_name')}",
|
| 1213 |
+
"date": result.get("advisory_date"),
|
| 1214 |
+
"advisory_id": result.get("advisory_id"),
|
| 1215 |
+
"crop": crop
|
| 1216 |
+
},
|
| 1217 |
+
"status": {
|
| 1218 |
+
"emoji": result.get("status_emoji"),
|
| 1219 |
+
"message": result.get("status_message"),
|
| 1220 |
+
"confidence": result.get("confidence_level")
|
| 1221 |
+
},
|
| 1222 |
+
"overview": {
|
| 1223 |
+
"situation": result.get("situation_summary"),
|
| 1224 |
+
"meaning": result.get("what_it_means")
|
| 1225 |
+
},
|
| 1226 |
+
"action_plan": {
|
| 1227 |
+
"urgent": result.get("urgent_today", []),
|
| 1228 |
+
"this_week": result.get("this_week", []),
|
| 1229 |
+
"long_term": result.get("plan_ahead", [])
|
| 1230 |
+
},
|
| 1231 |
+
"weather_section": {
|
| 1232 |
+
"outlook": result.get("weather_outlook"),
|
| 1233 |
+
"impact": result.get("weather_impact"),
|
| 1234 |
+
"tips": result.get("weather_tips", [])
|
| 1235 |
+
},
|
| 1236 |
+
"soil_section": {
|
| 1237 |
+
"health": result.get("soil_health_status"),
|
| 1238 |
+
"advice": result.get("nutrition_advice"),
|
| 1239 |
+
"tips": result.get("soil_tips", [])
|
| 1240 |
+
},
|
| 1241 |
+
"water_management": {
|
| 1242 |
+
"irrigation": result.get("irrigation_guidance"),
|
| 1243 |
+
"savings": result.get("water_saving_tips", [])
|
| 1244 |
+
},
|
| 1245 |
+
"pest_disease": {
|
| 1246 |
+
"risk_level": result.get("pest_risk_level"),
|
| 1247 |
+
"advice": result.get("pest_advice"),
|
| 1248 |
+
"prevention": result.get("prevention_steps", [])
|
| 1249 |
+
},
|
| 1250 |
+
"harvest_planning": {
|
| 1251 |
+
"readiness": result.get("harvest_readiness"),
|
| 1252 |
+
"timing": result.get("harvest_timing"),
|
| 1253 |
+
"tips": result.get("harvest_tips", []),
|
| 1254 |
+
"yield_expectation": result.get("yield_expectation")
|
| 1255 |
+
},
|
| 1256 |
+
"progress_report": {
|
| 1257 |
+
"going_well": result.get("what_going_well", []),
|
| 1258 |
+
"improve": result.get("areas_to_improve", [])
|
| 1259 |
+
},
|
| 1260 |
+
"support": {
|
| 1261 |
+
"resources": result.get("helpful_resources", []),
|
| 1262 |
+
"contact": result.get("contact_support")
|
| 1263 |
+
},
|
| 1264 |
+
"footer": {
|
| 1265 |
+
"next_check": result.get("next_advisory_date"),
|
| 1266 |
+
"key_reminders": result.get("key_reminders", [])
|
| 1267 |
+
}
|
| 1268 |
+
}
|
| 1269 |
+
|
| 1270 |
+
return report
|
| 1271 |
+
except Exception as e:
|
| 1272 |
+
raise HTTPException(status_code=500, detail=f"Advisory report error: {str(e)}")
|
| 1273 |
+
|
| 1274 |
+
@app.post("/advisory/compare", tags=["advisory"])
|
| 1275 |
+
async def compare_advisories(locations: List[str], crop: str):
|
| 1276 |
+
"""
|
| 1277 |
+
Compare advisories for multiple locations.
|
| 1278 |
+
Useful for farmers with multiple plots or cooperatives.
|
| 1279 |
+
"""
|
| 1280 |
+
results = {}
|
| 1281 |
+
for location in locations:
|
| 1282 |
+
try:
|
| 1283 |
+
advisory = await run_advisory_agent(location, crop)
|
| 1284 |
+
results[location] = {
|
| 1285 |
+
"status": advisory.get("status_message"),
|
| 1286 |
+
"risk_emoji": advisory.get("status_emoji"),
|
| 1287 |
+
"urgent_actions": advisory.get("urgent_today", []),
|
| 1288 |
+
"irrigation": advisory.get("irrigation_guidance"),
|
| 1289 |
+
"pest_risk": advisory.get("pest_risk_level")
|
| 1290 |
+
}
|
| 1291 |
+
except Exception as e:
|
| 1292 |
+
results[location] = {"error": str(e)}
|
| 1293 |
+
|
| 1294 |
+
return {
|
| 1295 |
+
"crop": crop,
|
| 1296 |
+
"comparison_date": datetime.now().strftime("%B %d, %Y"),
|
| 1297 |
+
"locations": results
|
| 1298 |
+
}
|
| 1299 |
+
|
| 1300 |
+
@app.get("/advisory/sms-format", tags=["advisory"])
|
| 1301 |
+
async def get_sms_advisory(
|
| 1302 |
+
location: str = Query(..., description="Farm location"),
|
| 1303 |
+
crop: str = Query(..., description="Crop name"),
|
| 1304 |
+
city: Optional[str] = Query(None, description="City for weather")
|
| 1305 |
+
):
|
| 1306 |
+
"""
|
| 1307 |
+
Get ultra-short advisory formatted for SMS (160 characters).
|
| 1308 |
+
Perfect for farmers with basic phones or limited data.
|
| 1309 |
+
"""
|
| 1310 |
+
try:
|
| 1311 |
+
result = await run_advisory_agent(location, crop, city)
|
| 1312 |
+
|
| 1313 |
+
# Create ultra-short SMS message
|
| 1314 |
+
status = result.get("status_emoji", "")
|
| 1315 |
+
urgent = result.get("urgent_today", ["Check farm"])[0][:60]
|
| 1316 |
+
irrigation = result.get("irrigation_guidance", "")[:40]
|
| 1317 |
+
|
| 1318 |
+
sms_message = f"{status} {crop} at {location}: {urgent}. Water: {irrigation}"
|
| 1319 |
+
|
| 1320 |
+
# Truncate to 160 chars
|
| 1321 |
+
sms_message = sms_message[:160]
|
| 1322 |
+
|
| 1323 |
+
return {
|
| 1324 |
+
"sms_text": sms_message,
|
| 1325 |
+
"character_count": len(sms_message),
|
| 1326 |
+
"full_advisory_url": f"/advisory?location={location}&crop={crop}"
|
| 1327 |
+
}
|
| 1328 |
+
except Exception as e:
|
| 1329 |
+
raise HTTPException(status_code=500, detail=f"SMS advisory error: {str(e)}")
|
| 1330 |
+
|
| 1331 |
+
@app.get("/advisory/voice", tags=["advisory"])
|
| 1332 |
+
async def get_voice_advisory(
|
| 1333 |
+
location: str = Query(..., description="Farm location"),
|
| 1334 |
+
crop: str = Query(..., description="Crop name"),
|
| 1335 |
+
city: Optional[str] = Query(None, description="City for weather")
|
| 1336 |
+
):
|
| 1337 |
+
"""
|
| 1338 |
+
Get advisory formatted for voice/audio output.
|
| 1339 |
+
Optimized for text-to-speech systems or voice calls.
|
| 1340 |
+
"""
|
| 1341 |
+
try:
|
| 1342 |
+
result = await run_advisory_agent(location, crop, city)
|
| 1343 |
+
|
| 1344 |
+
# Create voice-friendly script
|
| 1345 |
+
voice_script = f"""
|
| 1346 |
+
Hello farmer. This is your advisory for {crop} at {location}.
|
| 1347 |
+
|
| 1348 |
+
Status: {result.get('status_message', 'Checking conditions')}
|
| 1349 |
+
|
| 1350 |
+
Here's what you need to know: {result.get('situation_summary', 'Conditions are being monitored')}
|
| 1351 |
+
|
| 1352 |
+
Important actions for today:
|
| 1353 |
+
"""
|
| 1354 |
+
|
| 1355 |
+
for i, action in enumerate(result.get('urgent_today', [])[:3], 1):
|
| 1356 |
+
voice_script += f"\n{i}. {action}"
|
| 1357 |
+
|
| 1358 |
+
voice_script += f"\n\nFor watering: {result.get('irrigation_guidance', 'Check soil moisture')}"
|
| 1359 |
+
voice_script += f"\n\nPest risk is: {result.get('pest_risk_level', 'moderate')}. {result.get('pest_advice', 'Monitor your crops')}"
|
| 1360 |
+
voice_script += "\n\nThank you for listening. Check back in a few days for updates."
|
| 1361 |
+
|
| 1362 |
+
return {
|
| 1363 |
+
"voice_script": voice_script,
|
| 1364 |
+
"estimated_duration_seconds": len(voice_script.split()) * 0.4, # ~150 words/min
|
| 1365 |
+
"language": "english"
|
| 1366 |
+
}
|
| 1367 |
+
except Exception as e:
|
| 1368 |
+
raise HTTPException(status_code=500, detail=f"Voice advisory error: {str(e)}")
|
| 1369 |
+
|
| 1370 |
+
# ============================================================
|
| 1371 |
+
# COMPARISON ENDPOINT: Prediction vs Advisory
|
| 1372 |
+
# ============================================================
|
| 1373 |
+
|
| 1374 |
+
@app.get("/compare/prediction-advisory", tags=["integrated"])
|
| 1375 |
+
async def compare_prediction_and_advisory(
|
| 1376 |
+
location: str = Query(..., description="Farm location"),
|
| 1377 |
+
crop: str = Query(..., description="Crop name"),
|
| 1378 |
+
city: Optional[str] = Query(None, description="City for weather")
|
| 1379 |
+
):
|
| 1380 |
+
"""
|
| 1381 |
+
Show side-by-side comparison of raw prediction vs farmer-friendly advisory.
|
| 1382 |
+
Useful for understanding the transformation process.
|
| 1383 |
+
"""
|
| 1384 |
+
try:
|
| 1385 |
+
from ageents.predictionAgent import run_prediction_workflow
|
| 1386 |
+
|
| 1387 |
+
# Get both
|
| 1388 |
+
prediction = await run_prediction_workflow(location, crop, city)
|
| 1389 |
+
advisory = await run_advisory_agent(location, crop, city)
|
| 1390 |
+
|
| 1391 |
+
return {
|
| 1392 |
+
"location": location,
|
| 1393 |
+
"crop": crop,
|
| 1394 |
+
"comparison": {
|
| 1395 |
+
"raw_prediction": {
|
| 1396 |
+
"risk_level": prediction.get("risk_level"),
|
| 1397 |
+
"success_probability": prediction.get("success_probability"),
|
| 1398 |
+
"immediate_actions": prediction.get("immediate_actions", []),
|
| 1399 |
+
"executive_summary": prediction.get("executive_summary")
|
| 1400 |
+
},
|
| 1401 |
+
"farmer_advisory": {
|
| 1402 |
+
"status_message": advisory.get("status_message"),
|
| 1403 |
+
"situation_summary": advisory.get("situation_summary"),
|
| 1404 |
+
"urgent_today": advisory.get("urgent_today", []),
|
| 1405 |
+
"what_it_means": advisory.get("what_it_means")
|
| 1406 |
+
}
|
| 1407 |
+
},
|
| 1408 |
+
"note": "Advisory transforms technical data into farmer-friendly language"
|
| 1409 |
+
}
|
| 1410 |
+
except Exception as e:
|
| 1411 |
+
raise HTTPException(status_code=500, detail=f"Comparison error: {str(e)}")
|
| 1412 |
+
|
| 1413 |
+
from ageents.marketAdvisoryAgent import run_market_advisory
|
| 1414 |
+
from pydantic import BaseModel
|
| 1415 |
+
from typing import Optional, List
|
| 1416 |
+
|
| 1417 |
+
class MarketAdvisoryRequest(BaseModel):
|
| 1418 |
+
crop: str
|
| 1419 |
+
budget: Optional[float] = None
|
| 1420 |
+
land_size: Optional[float] = None
|
| 1421 |
+
risk_tolerance: Optional[str] = None
|
| 1422 |
+
|
| 1423 |
+
# ============================================================
|
| 1424 |
+
# MARKET ADVISORY ENDPOINTS
|
| 1425 |
+
# ============================================================
|
| 1426 |
+
|
| 1427 |
+
@app.get("/market-advisory/selling-guide", tags=["market-advisory"])
|
| 1428 |
+
async def get_selling_guide(
|
| 1429 |
+
crop: str = Query(..., description="Crop name")
|
| 1430 |
+
):
|
| 1431 |
+
"""
|
| 1432 |
+
Get complete selling guide with tips, timing, and negotiation advice.
|
| 1433 |
+
Helps farmers get the best price when selling their crop.
|
| 1434 |
+
"""
|
| 1435 |
+
try:
|
| 1436 |
+
result = await run_market_advisory(crop)
|
| 1437 |
+
|
| 1438 |
+
return {
|
| 1439 |
+
"crop": crop,
|
| 1440 |
+
"current_price": result.get("current_price_pkr"),
|
| 1441 |
+
"is_good_time_to_sell": result.get("is_good_price"),
|
| 1442 |
+
"when_to_sell": result.get("when_to_act"),
|
| 1443 |
+
"fair_price_range": result.get("fair_price_range"),
|
| 1444 |
+
"minimum_price": result.get("dont_sell_below"),
|
| 1445 |
+
"target_price": result.get("aim_to_sell_at"),
|
| 1446 |
+
"selling_tips": result.get("selling_tips", []),
|
| 1447 |
+
"market_access_tips": result.get("market_access_tips", []),
|
| 1448 |
+
"transport_costs": result.get("transport_costs"),
|
| 1449 |
+
"cooperative_opportunities": result.get("cooperative_opportunities", [])
|
| 1450 |
+
}
|
| 1451 |
+
except Exception as e:
|
| 1452 |
+
raise HTTPException(status_code=500, detail=f"Selling guide error: {str(e)}")
|
| 1453 |
+
|
| 1454 |
+
@app.get("/market-advisory/buying-guide", tags=["market-advisory"])
|
| 1455 |
+
async def get_buying_guide(
|
| 1456 |
+
crop: str = Query(..., description="Crop name for which to buy inputs")
|
| 1457 |
+
):
|
| 1458 |
+
"""
|
| 1459 |
+
Get buying guide for agricultural inputs.
|
| 1460 |
+
Helps farmers save money when purchasing seeds, fertilizer, etc.
|
| 1461 |
+
"""
|
| 1462 |
+
try:
|
| 1463 |
+
result = await run_market_advisory(crop)
|
| 1464 |
+
|
| 1465 |
+
return {
|
| 1466 |
+
"crop": crop,
|
| 1467 |
+
"main_advice": result.get("main_action"),
|
| 1468 |
+
"best_time_to_buy": result.get("when_to_act"),
|
| 1469 |
+
"buying_tips": result.get("buying_tips", []),
|
| 1470 |
+
"cooperative_opportunities": result.get("cooperative_opportunities", []),
|
| 1471 |
+
"seasonal_considerations": result.get("seasonal_advice")
|
| 1472 |
+
}
|
| 1473 |
+
except Exception as e:
|
| 1474 |
+
raise HTTPException(status_code=500, detail=f"Buying guide error: {str(e)}")
|
| 1475 |
+
|
| 1476 |
+
@app.get("/market-advisory/sms", tags=["market-advisory"])
|
| 1477 |
+
async def get_sms_market_advisory(
|
| 1478 |
+
crop: str = Query(..., description="Crop name")
|
| 1479 |
+
):
|
| 1480 |
+
"""
|
| 1481 |
+
Get ultra-short market advisory formatted for SMS (160 characters).
|
| 1482 |
+
Perfect for farmers with basic phones or limited data.
|
| 1483 |
+
"""
|
| 1484 |
+
try:
|
| 1485 |
+
result = await run_market_advisory(crop)
|
| 1486 |
+
|
| 1487 |
+
emoji = result.get("decision_emoji", "")
|
| 1488 |
+
action = result.get("main_action", "Check market")[:30]
|
| 1489 |
+
price = result.get("current_price_pkr", 0)
|
| 1490 |
+
direction = result.get("price_direction", "")[:20]
|
| 1491 |
+
|
| 1492 |
+
sms_text = f"{emoji} {crop.upper()}: {action}. Price: โจ{price:,.0f}. Trend: {direction}"
|
| 1493 |
+
sms_text = sms_text[:160]
|
| 1494 |
+
|
| 1495 |
+
return {
|
| 1496 |
+
"sms_text": sms_text,
|
| 1497 |
+
"character_count": len(sms_text),
|
| 1498 |
+
"full_advisory_url": f"/market-advisory/quick?crop={crop}"
|
| 1499 |
+
}
|
| 1500 |
+
except Exception as e:
|
| 1501 |
+
raise HTTPException(status_code=500, detail=f"SMS advisory error: {str(e)}")
|
| 1502 |
+
|
| 1503 |
+
@app.get("/market-advisory/voice", tags=["market-advisory"])
|
| 1504 |
+
async def get_voice_market_advisory(
|
| 1505 |
+
crop: str = Query(..., description="Crop name")
|
| 1506 |
+
):
|
| 1507 |
+
"""
|
| 1508 |
+
Get market advisory formatted for voice/audio output.
|
| 1509 |
+
Optimized for text-to-speech systems or voice calls.
|
| 1510 |
+
"""
|
| 1511 |
+
try:
|
| 1512 |
+
result = await run_market_advisory(crop)
|
| 1513 |
+
|
| 1514 |
+
voice_script = f"""
|
| 1515 |
+
Hello farmer. This is your market advisory for {crop}.
|
| 1516 |
+
|
| 1517 |
+
Current situation: {result.get('what_happening', 'Market conditions are being analyzed')}
|
| 1518 |
+
|
| 1519 |
+
Current price is {result.get('current_price_pkr', 0)} rupees per 40 kilograms.
|
| 1520 |
+
Price trend: {result.get('price_direction', 'stable')}
|
| 1521 |
+
|
| 1522 |
+
My advice: {result.get('main_action', 'Monitor market closely')}
|
| 1523 |
+
|
| 1524 |
+
Action for today:
|
| 1525 |
+
"""
|
| 1526 |
+
|
| 1527 |
+
for i, action in enumerate(result.get('do_today', [])[:3], 1):
|
| 1528 |
+
voice_script += f"\n{i}. {action}"
|
| 1529 |
+
|
| 1530 |
+
voice_script += f"\n\nTiming: {result.get('when_to_act', 'Monitor regularly')}"
|
| 1531 |
+
voice_script += f"\n\nBottom line: {result.get('bottom_line', 'Stay informed and act wisely')}"
|
| 1532 |
+
voice_script += "\n\nThank you for listening. Check back regularly for updates."
|
| 1533 |
+
|
| 1534 |
+
return {
|
| 1535 |
+
"voice_script": voice_script,
|
| 1536 |
+
"estimated_duration_seconds": len(voice_script.split()) * 0.4,
|
| 1537 |
+
"language": "english"
|
| 1538 |
+
}
|
| 1539 |
+
except Exception as e:
|
| 1540 |
+
raise HTTPException(status_code=500, detail=f"Voice advisory error: {str(e)}")
|
| 1541 |
+
|
| 1542 |
+
@app.get("/market-advisory/report", tags=["market-advisory"])
|
| 1543 |
+
async def get_market_advisory_report(
|
| 1544 |
+
crop: str = Query(..., description="Crop name"),
|
| 1545 |
+
budget: Optional[float] = Query(None),
|
| 1546 |
+
land_size: Optional[float] = Query(None),
|
| 1547 |
+
risk_tolerance: Optional[str] = Query(None)
|
| 1548 |
+
):
|
| 1549 |
+
"""
|
| 1550 |
+
Get complete market advisory report formatted for printing or sharing.
|
| 1551 |
+
Perfect for farmers who want a full reference document.
|
| 1552 |
+
"""
|
| 1553 |
+
try:
|
| 1554 |
+
farmer_context = {}
|
| 1555 |
+
if budget:
|
| 1556 |
+
farmer_context["budget"] = budget
|
| 1557 |
+
if land_size:
|
| 1558 |
+
farmer_context["land_size"] = land_size
|
| 1559 |
+
if risk_tolerance:
|
| 1560 |
+
farmer_context["risk_tolerance"] = risk_tolerance
|
| 1561 |
+
|
| 1562 |
+
result = await run_market_advisory(crop, farmer_context or None)
|
| 1563 |
+
|
| 1564 |
+
report = {
|
| 1565 |
+
"header": {
|
| 1566 |
+
"title": f"Market Advisory Report - {crop.upper()}",
|
| 1567 |
+
"date": result.get("advisory_date"),
|
| 1568 |
+
"advisory_id": result.get("advisory_id")
|
| 1569 |
+
},
|
| 1570 |
+
"decision": {
|
| 1571 |
+
"emoji": result.get("decision_emoji"),
|
| 1572 |
+
"action": result.get("main_action"),
|
| 1573 |
+
"urgency": result.get("urgency_level"),
|
| 1574 |
+
"when": result.get("when_to_act")
|
| 1575 |
+
},
|
| 1576 |
+
"market_situation": {
|
| 1577 |
+
"what_happening": result.get("what_happening"),
|
| 1578 |
+
"why_matters": result.get("why_it_matters"),
|
| 1579 |
+
"best_move": result.get("best_move")
|
| 1580 |
+
},
|
| 1581 |
+
"price_information": {
|
| 1582 |
+
"current_price": result.get("current_price_pkr"),
|
| 1583 |
+
"price_direction": result.get("price_direction"),
|
| 1584 |
+
"price_change": result.get("price_change_description"),
|
| 1585 |
+
"fair_range": result.get("fair_price_range"),
|
| 1586 |
+
"minimum_acceptable": result.get("dont_sell_below"),
|
| 1587 |
+
"target_price": result.get("aim_to_sell_at")
|
| 1588 |
+
},
|
| 1589 |
+
"action_plan": {
|
| 1590 |
+
"today": result.get("do_today", []),
|
| 1591 |
+
"this_week": result.get("do_this_week", []),
|
| 1592 |
+
"future": result.get("plan_ahead", [])
|
| 1593 |
+
},
|
| 1594 |
+
"financial_outlook": {
|
| 1595 |
+
"expected_income": result.get("expected_income_per_acre"),
|
| 1596 |
+
"profit_potential": result.get("profit_potential"),
|
| 1597 |
+
"price_forecast": result.get("price_forecast_simple")
|
| 1598 |
+
},
|
| 1599 |
+
"risks_and_protection": {
|
| 1600 |
+
"risks": result.get("risks_to_watch", []),
|
| 1601 |
+
"protection": result.get("how_to_protect", []),
|
| 1602 |
+
"alternatives": result.get("if_cant_sell_now", [])
|
| 1603 |
+
},
|
| 1604 |
+
"market_context": {
|
| 1605 |
+
"supply": result.get("supply_situation"),
|
| 1606 |
+
"demand": result.get("demand_situation"),
|
| 1607 |
+
"competition": result.get("competition_level"),
|
| 1608 |
+
"seasonal": result.get("seasonal_advice")
|
| 1609 |
+
},
|
| 1610 |
+
"practical_tips": {
|
| 1611 |
+
"selling": result.get("selling_tips", []),
|
| 1612 |
+
"buying": result.get("buying_tips", []),
|
| 1613 |
+
"storage": result.get("storage_advice"),
|
| 1614 |
+
"transport": result.get("transport_costs")
|
| 1615 |
+
},
|
| 1616 |
+
"resources": {
|
| 1617 |
+
"price_sources": result.get("where_to_check_prices", []),
|
| 1618 |
+
"contacts": result.get("who_to_contact", []),
|
| 1619 |
+
"cooperative": result.get("cooperative_opportunities", [])
|
| 1620 |
+
},
|
| 1621 |
+
"summary": {
|
| 1622 |
+
"bottom_line": result.get("bottom_line"),
|
| 1623 |
+
"confidence": result.get("confidence_level")
|
| 1624 |
+
}
|
| 1625 |
+
}
|
| 1626 |
+
|
| 1627 |
+
return report
|
| 1628 |
+
except Exception as e:
|
| 1629 |
+
raise HTTPException(status_code=500, detail=f"Report generation error: {str(e)}")
|
| 1630 |
+
|
| 1631 |
+
@app.post("/market-advisory/compare", tags=["market-advisory"])
|
| 1632 |
+
async def compare_market_advisories(
|
| 1633 |
+
crops: List[str],
|
| 1634 |
+
budget: Optional[float] = None,
|
| 1635 |
+
land_size: Optional[float] = None
|
| 1636 |
+
):
|
| 1637 |
+
"""
|
| 1638 |
+
Compare market advisories for multiple crops.
|
| 1639 |
+
Useful for farmers deciding which crop to plant or sell.
|
| 1640 |
+
"""
|
| 1641 |
+
results = {}
|
| 1642 |
+
farmer_context = {}
|
| 1643 |
+
if budget:
|
| 1644 |
+
farmer_context["budget"] = budget
|
| 1645 |
+
if land_size:
|
| 1646 |
+
farmer_context["land_size"] = land_size
|
| 1647 |
+
|
| 1648 |
+
for crop in crops:
|
| 1649 |
+
try:
|
| 1650 |
+
advisory = await run_market_advisory(crop, farmer_context or None)
|
| 1651 |
+
results[crop] = {
|
| 1652 |
+
"decision": advisory.get("main_action"),
|
| 1653 |
+
"emoji": advisory.get("decision_emoji"),
|
| 1654 |
+
"urgency": advisory.get("urgency_level"),
|
| 1655 |
+
"current_price": advisory.get("current_price_pkr"),
|
| 1656 |
+
"price_direction": advisory.get("price_direction"),
|
| 1657 |
+
"profit_potential": advisory.get("profit_potential"),
|
| 1658 |
+
"when_to_act": advisory.get("when_to_act"),
|
| 1659 |
+
"bottom_line": advisory.get("bottom_line")
|
| 1660 |
+
}
|
| 1661 |
+
except Exception as e:
|
| 1662 |
+
results[crop] = {"error": str(e)}
|
| 1663 |
+
|
| 1664 |
+
# Determine best option
|
| 1665 |
+
best_crop = None
|
| 1666 |
+
best_score = -1
|
| 1667 |
+
|
| 1668 |
+
for crop, data in results.items():
|
| 1669 |
+
if "error" not in data:
|
| 1670 |
+
score = 0
|
| 1671 |
+
if "SELL" in data.get("decision", "").upper() and data.get("urgency") == "urgent":
|
| 1672 |
+
score += 3
|
| 1673 |
+
if "going up" in data.get("price_direction", ""):
|
| 1674 |
+
score += 2
|
| 1675 |
+
if data.get("profit_potential") == "Good profit expected":
|
| 1676 |
+
score += 2
|
| 1677 |
+
|
| 1678 |
+
if score > best_score:
|
| 1679 |
+
best_score = score
|
| 1680 |
+
best_crop = crop
|
| 1681 |
+
|
| 1682 |
+
return {
|
| 1683 |
+
"comparison_date": datetime.now().strftime("%B %d, %Y"),
|
| 1684 |
+
"farmer_context": farmer_context,
|
| 1685 |
+
"crops": results,
|
| 1686 |
+
"recommendation": f"Based on current conditions, {best_crop} looks most promising" if best_crop else "Monitor all crops closely"
|
| 1687 |
+
}
|
| 1688 |
+
|
| 1689 |
+
# ============================================================
|
| 1690 |
+
# COMPARISON ENDPOINT: Raw Market vs Advisory
|
| 1691 |
+
# ============================================================
|
| 1692 |
+
|
| 1693 |
+
@app.get("/compare/market-advisory", tags=["integrated"])
|
| 1694 |
+
async def compare_market_and_advisory(
|
| 1695 |
+
crop: str = Query(..., description="Crop name"),
|
| 1696 |
+
budget: Optional[float] = Query(None),
|
| 1697 |
+
land_size: Optional[float] = Query(None)
|
| 1698 |
+
):
|
| 1699 |
+
"""
|
| 1700 |
+
Show side-by-side comparison of raw market analysis vs farmer-friendly advisory.
|
| 1701 |
+
Useful for understanding the transformation process.
|
| 1702 |
+
"""
|
| 1703 |
+
try:
|
| 1704 |
+
from ageents.marketAgent import get_market_advice
|
| 1705 |
+
|
| 1706 |
+
farmer_context = {}
|
| 1707 |
+
if budget:
|
| 1708 |
+
farmer_context["budget"] = budget
|
| 1709 |
+
if land_size:
|
| 1710 |
+
farmer_context["land_size"] = land_size
|
| 1711 |
+
|
| 1712 |
+
# Get both
|
| 1713 |
+
raw_market = await get_market_advice(crop, farmer_context or None)
|
| 1714 |
+
advisory = await run_market_advisory(crop, farmer_context or None)
|
| 1715 |
+
|
| 1716 |
+
return {
|
| 1717 |
+
"crop": crop,
|
| 1718 |
+
"comparison": {
|
| 1719 |
+
"raw_market_analysis": {
|
| 1720 |
+
"type": "Technical",
|
| 1721 |
+
"format": "Markdown with data points",
|
| 1722 |
+
"preview": raw_market[:200] + "..." if len(raw_market) > 200 else raw_market,
|
| 1723 |
+
"full_length": len(raw_market)
|
| 1724 |
+
},
|
| 1725 |
+
"farmer_advisory": {
|
| 1726 |
+
"type": "Farmer-Friendly",
|
| 1727 |
+
"format": "Structured JSON",
|
| 1728 |
+
"decision": advisory.get("main_action"),
|
| 1729 |
+
"price": advisory.get("current_price_pkr"),
|
| 1730 |
+
"bottom_line": advisory.get("bottom_line")
|
| 1731 |
+
}
|
| 1732 |
+
},
|
| 1733 |
+
"transformation_summary": {
|
| 1734 |
+
"technical_to_simple": "Converts percentages to rupee amounts",
|
| 1735 |
+
"action_oriented": "Provides clear YES/NO/WAIT decisions",
|
| 1736 |
+
"practical_focus": "Includes transport, storage, negotiation tips",
|
| 1737 |
+
"money_focused": "Shows expected income per acre"
|
| 1738 |
+
},
|
| 1739 |
+
"note": "Advisory transforms technical market data into actionable farmer advice"
|
| 1740 |
+
}
|
| 1741 |
+
except Exception as e:
|
| 1742 |
+
raise HTTPException(status_code=500, detail=f"Comparison error: {str(e)}")
|
| 1743 |
+
|
| 1744 |
+
# ============================================================
|
| 1745 |
+
# INTEGRATED WORKFLOW: Farm Advisory + Market Advisory
|
| 1746 |
+
# ============================================================
|
| 1747 |
+
|
| 1748 |
+
@app.get("/integrated/complete-advisory", tags=["integrated"])
|
| 1749 |
+
async def get_complete_farm_advisory(
|
| 1750 |
+
location: str = Query(..., description="Farm location"),
|
| 1751 |
+
crop: str = Query(..., description="Crop name"),
|
| 1752 |
+
city: Optional[str] = Query(None, description="City for weather"),
|
| 1753 |
+
budget: Optional[float] = Query(None, description="Budget in PKR"),
|
| 1754 |
+
land_size: Optional[float] = Query(None, description="Land size in acres")
|
| 1755 |
+
):
|
| 1756 |
+
"""
|
| 1757 |
+
Get complete farm advisory: Weather + Soil + Prediction + Market.
|
| 1758 |
+
One-stop endpoint for all farming decisions.
|
| 1759 |
+
"""
|
| 1760 |
+
try:
|
| 1761 |
+
from ageents.advisoryAgent import run_advisory_agent
|
| 1762 |
+
|
| 1763 |
+
farmer_context = {}
|
| 1764 |
+
if budget:
|
| 1765 |
+
farmer_context["budget"] = budget
|
| 1766 |
+
if land_size:
|
| 1767 |
+
farmer_context["land_size"] = land_size
|
| 1768 |
+
|
| 1769 |
+
# Get farm advisory (weather + soil + prediction)
|
| 1770 |
+
print("๐ Fetching farm advisory...")
|
| 1771 |
+
farm_advisory = await run_advisory_agent(location, crop, city)
|
| 1772 |
+
|
| 1773 |
+
# Get market advisory
|
| 1774 |
+
print("๐ฐ Fetching market advisory...")
|
| 1775 |
+
market_advisory = await run_market_advisory(crop, farmer_context or None)
|
| 1776 |
+
|
| 1777 |
+
return {
|
| 1778 |
+
"location": location,
|
| 1779 |
+
"crop": crop,
|
| 1780 |
+
"advisory_date": datetime.now().strftime("%B %d, %Y"),
|
| 1781 |
+
|
| 1782 |
+
"farm_conditions": {
|
| 1783 |
+
"status": farm_advisory.get("status_message"),
|
| 1784 |
+
"status_emoji": farm_advisory.get("status_emoji"),
|
| 1785 |
+
"situation": farm_advisory.get("situation_summary"),
|
| 1786 |
+
"urgent_actions": farm_advisory.get("urgent_today", []),
|
| 1787 |
+
"irrigation": farm_advisory.get("irrigation_guidance"),
|
| 1788 |
+
"pest_risk": farm_advisory.get("pest_risk_level")
|
| 1789 |
+
},
|
| 1790 |
+
|
| 1791 |
+
"market_conditions": {
|
| 1792 |
+
"decision": market_advisory.get("main_action"),
|
| 1793 |
+
"decision_emoji": market_advisory.get("decision_emoji"),
|
| 1794 |
+
"current_price": market_advisory.get("current_price_pkr"),
|
| 1795 |
+
"price_direction": market_advisory.get("price_direction"),
|
| 1796 |
+
"when_to_act": market_advisory.get("when_to_act"),
|
| 1797 |
+
"profit_potential": market_advisory.get("profit_potential")
|
| 1798 |
+
},
|
| 1799 |
+
|
| 1800 |
+
"integrated_recommendations": {
|
| 1801 |
+
"farming_actions": farm_advisory.get("urgent_today", [])[:3],
|
| 1802 |
+
"market_actions": market_advisory.get("do_today", [])[:3],
|
| 1803 |
+
"overall_strategy": combine_advisories(farm_advisory, market_advisory, crop)
|
| 1804 |
+
},
|
| 1805 |
+
|
| 1806 |
+
"financial_outlook": {
|
| 1807 |
+
"expected_income": market_advisory.get("expected_income_per_acre"),
|
| 1808 |
+
"yield_expectation": farm_advisory.get("yield_expectation"),
|
| 1809 |
+
"success_probability": f"{farm_advisory.get('confidence_level', 'Moderate')} confidence"
|
| 1810 |
+
},
|
| 1811 |
+
|
| 1812 |
+
"next_steps": {
|
| 1813 |
+
"immediate": "Focus on urgent farming tasks and monitor market prices",
|
| 1814 |
+
"this_week": "Follow irrigation schedule and prepare for selling if advised",
|
| 1815 |
+
"planning": "Consider seasonal patterns and long-term soil improvement"
|
| 1816 |
+
}
|
| 1817 |
+
}
|
| 1818 |
+
except Exception as e:
|
| 1819 |
+
raise HTTPException(status_code=500, detail=f"Complete advisory error: {str(e)}")
|
| 1820 |
+
|
| 1821 |
+
# ============================================================
|
| 1822 |
+
# HELPER FUNCTION
|
| 1823 |
+
# ============================================================
|
| 1824 |
+
|
| 1825 |
+
def combine_advisories(farm_advisory: dict, market_advisory: dict, crop: str) -> str:
|
| 1826 |
+
"""Combine farm and market advisories into integrated strategy"""
|
| 1827 |
+
|
| 1828 |
+
farm_status = farm_advisory.get("status_emoji", "๐ก")
|
| 1829 |
+
market_action = market_advisory.get("main_action", "Monitor")
|
| 1830 |
+
|
| 1831 |
+
if "๐ข" in farm_status and "SELL" in market_action.upper():
|
| 1832 |
+
return f"Perfect timing! Your {crop} is healthy AND market prices are good. Focus on harvest prep and selling."
|
| 1833 |
+
|
| 1834 |
+
elif "๐ก" in farm_status and "SELL" in market_action.upper():
|
| 1835 |
+
return f"Market is good for selling, but improve crop health first. Address urgent farming tasks before harvest."
|
| 1836 |
+
|
| 1837 |
+
elif "๐ด" in farm_status:
|
| 1838 |
+
return f"Priority: Fix crop health issues immediately. Market considerations are secondary until crop improves."
|
| 1839 |
+
|
| 1840 |
+
elif "WAIT" in market_action.upper() or "HOLD" in market_action.upper():
|
| 1841 |
+
return f"Focus on farming tasks - market timing isn't urgent. Improve yield quality while waiting for better prices."
|
| 1842 |
+
|
| 1843 |
+
elif "BUY" in market_action.upper():
|
| 1844 |
+
return f"Good time to buy inputs for next season. Meanwhile, maintain current crop properly for decent harvest."
|
| 1845 |
+
|
| 1846 |
+
else:
|
| 1847 |
+
return f"Balanced approach: Maintain farm health and monitor market conditions regularly.".post("/market-advisory", tags=["market-advisory"])
|
| 1848 |
+
async def get_market_advisory(request: MarketAdvisoryRequest):
|
| 1849 |
+
"""
|
| 1850 |
+
Get farmer-friendly market advisory (comprehensive).
|
| 1851 |
+
Transforms raw market data into practical, money-focused advice.
|
| 1852 |
+
"""
|
| 1853 |
+
try:
|
| 1854 |
+
farmer_context = {}
|
| 1855 |
+
if request.budget:
|
| 1856 |
+
farmer_context["budget"] = request.budget
|
| 1857 |
+
if request.land_size:
|
| 1858 |
+
farmer_context["land_size"] = request.land_size
|
| 1859 |
+
if request.risk_tolerance:
|
| 1860 |
+
farmer_context["risk_tolerance"] = request.risk_tolerance
|
| 1861 |
+
|
| 1862 |
+
result = await run_market_advisory(
|
| 1863 |
+
crop=request.crop,
|
| 1864 |
+
farmer_context=farmer_context or None
|
| 1865 |
+
)
|
| 1866 |
+
return result
|
| 1867 |
+
except Exception as e:
|
| 1868 |
+
raise HTTPException(status_code=500, detail=f"Market advisory error: {str(e)}")
|
| 1869 |
+
|
| 1870 |
+
@app.get("/market-advisory/quick", tags=["market-advisory"])
|
| 1871 |
+
async def get_quick_market_advisory(
|
| 1872 |
+
crop: str = Query(..., description="Crop name"),
|
| 1873 |
+
budget: Optional[float] = Query(None, description="Budget in PKR"),
|
| 1874 |
+
land_size: Optional[float] = Query(None, description="Land size in acres"),
|
| 1875 |
+
risk_tolerance: Optional[str] = Query(None, description="low, medium, or high")
|
| 1876 |
+
):
|
| 1877 |
+
"""
|
| 1878 |
+
Quick market advisory - just the essentials for busy farmers.
|
| 1879 |
+
Returns: decision, current price, urgency, and top 3 actions.
|
| 1880 |
+
"""
|
| 1881 |
+
try:
|
| 1882 |
+
farmer_context = {}
|
| 1883 |
+
if budget:
|
| 1884 |
+
farmer_context["budget"] = budget
|
| 1885 |
+
if land_size:
|
| 1886 |
+
farmer_context["land_size"] = land_size
|
| 1887 |
+
if risk_tolerance:
|
| 1888 |
+
farmer_context["risk_tolerance"] = risk_tolerance
|
| 1889 |
+
|
| 1890 |
+
result = await run_market_advisory(crop, farmer_context or None)
|
| 1891 |
+
|
| 1892 |
+
quick_summary = {
|
| 1893 |
+
"decision_emoji": result.get("decision_emoji"),
|
| 1894 |
+
"main_action": result.get("main_action"),
|
| 1895 |
+
"urgency_level": result.get("urgency_level"),
|
| 1896 |
+
"current_price_pkr": result.get("current_price_pkr"),
|
| 1897 |
+
"price_direction": result.get("price_direction"),
|
| 1898 |
+
"when_to_act": result.get("when_to_act"),
|
| 1899 |
+
"do_today": result.get("do_today", [])[:3],
|
| 1900 |
+
"bottom_line": result.get("bottom_line")
|
| 1901 |
+
}
|
| 1902 |
+
return quick_summary
|
| 1903 |
+
except Exception as e:
|
| 1904 |
+
raise HTTPException(status_code=500, detail=f"Quick advisory error: {str(e)}")
|
| 1905 |
+
|
| 1906 |
+
@app.get("/market-advisory/decision", tags=["market-advisory"])
|
| 1907 |
+
async def get_market_decision(
|
| 1908 |
+
crop: str = Query(..., description="Crop name"),
|
| 1909 |
+
budget: Optional[float] = Query(None),
|
| 1910 |
+
land_size: Optional[float] = Query(None)
|
| 1911 |
+
):
|
| 1912 |
+
"""
|
| 1913 |
+
Get simple YES/NO/WAIT decision for selling or buying.
|
| 1914 |
+
Perfect for farmers who just need a quick answer.
|
| 1915 |
+
"""
|
| 1916 |
+
try:
|
| 1917 |
+
farmer_context = {}
|
| 1918 |
+
if budget:
|
| 1919 |
+
farmer_context["budget"] = budget
|
| 1920 |
+
if land_size:
|
| 1921 |
+
farmer_context["land_size"] = land_size
|
| 1922 |
+
|
| 1923 |
+
result = await run_market_advisory(crop, farmer_context or None)
|
| 1924 |
+
|
| 1925 |
+
return {
|
| 1926 |
+
"crop": crop,
|
| 1927 |
+
"decision": result.get("main_action"),
|
| 1928 |
+
"emoji": result.get("decision_emoji"),
|
| 1929 |
+
"urgency": result.get("urgency_level"),
|
| 1930 |
+
"simple_explanation": result.get("why_it_matters"),
|
| 1931 |
+
"when": result.get("when_to_act")
|
| 1932 |
+
}
|
| 1933 |
+
except Exception as e:
|
| 1934 |
+
raise HTTPException(status_code=500, detail=f"Decision error: {str(e)}")
|
| 1935 |
+
|
| 1936 |
+
@app.get("/market-advisory/price-info", tags=["market-advisory"])
|
| 1937 |
+
async def get_price_information(
|
| 1938 |
+
crop: str = Query(..., description="Crop name")
|
| 1939 |
+
):
|
| 1940 |
+
"""
|
| 1941 |
+
Get detailed price information and negotiation guidance.
|
| 1942 |
+
Includes fair price range, minimum acceptable price, and selling tips.
|
| 1943 |
+
"""
|
| 1944 |
+
try:
|
| 1945 |
+
result = await run_market_advisory(crop)
|
| 1946 |
+
|
| 1947 |
+
return {
|
| 1948 |
+
"crop": crop,
|
| 1949 |
+
"current_price": result.get("current_price_pkr"),
|
| 1950 |
+
"price_direction": result.get("price_direction"),
|
| 1951 |
+
"price_change": result.get("price_change_description"),
|
| 1952 |
+
"is_good_price": result.get("is_good_price"),
|
| 1953 |
+
"fair_price_range": result.get("fair_price_range"),
|
| 1954 |
+
"dont_sell_below": result.get("dont_sell_below"),
|
| 1955 |
+
"aim_to_sell_at": result.get("aim_to_sell_at"),
|
| 1956 |
+
"compared_to_last_week": result.get("compared_to_last_week"),
|
| 1957 |
+
"compared_to_last_month": result.get("compared_to_last_month"),
|
| 1958 |
+
"selling_tips": result.get("selling_tips", [])
|
| 1959 |
+
}
|
| 1960 |
+
except Exception as e:
|
| 1961 |
+
raise HTTPException(status_code=500, detail=f"Price info error: {str(e)}")
|
| 1962 |
+
|
| 1963 |
+
@app.get("/market-advisory/action-plan", tags=["market-advisory"])
|
| 1964 |
+
async def get_action_plan(
|
| 1965 |
+
crop: str = Query(..., description="Crop name"),
|
| 1966 |
+
budget: Optional[float] = Query(None),
|
| 1967 |
+
land_size: Optional[float] = Query(None)
|
| 1968 |
+
):
|
| 1969 |
+
"""
|
| 1970 |
+
Get time-based action plan: today, this week, and future planning.
|
| 1971 |
+
Clear checklist format for farmers.
|
| 1972 |
+
"""
|
| 1973 |
+
try:
|
| 1974 |
+
farmer_context = {}
|
| 1975 |
+
if budget:
|
| 1976 |
+
farmer_context["budget"] = budget
|
| 1977 |
+
if land_size:
|
| 1978 |
+
farmer_context["land_size"] = land_size
|
| 1979 |
+
|
| 1980 |
+
result = await run_market_advisory(crop, farmer_context or None)
|
| 1981 |
+
|
| 1982 |
+
return {
|
| 1983 |
+
"crop": crop,
|
| 1984 |
+
"main_action": result.get("main_action"),
|
| 1985 |
+
"urgency": result.get("urgency_level"),
|
| 1986 |
+
"actions": {
|
| 1987 |
+
"do_today": result.get("do_today", []),
|
| 1988 |
+
"do_this_week": result.get("do_this_week", []),
|
| 1989 |
+
"plan_ahead": result.get("plan_ahead", [])
|
| 1990 |
+
},
|
| 1991 |
+
"when_to_act": result.get("when_to_act")
|
| 1992 |
+
}
|
| 1993 |
+
except Exception as e:
|
| 1994 |
+
raise HTTPException(status_code=500, detail=f"Action plan error: {str(e)}")
|
| 1995 |
+
|
| 1996 |
+
@app.get("/market-advisory/profit-potential", tags=["market-advisory"])
|
| 1997 |
+
async def get_profit_potential(
|
| 1998 |
+
crop: str = Query(..., description="Crop name"),
|
| 1999 |
+
land_size: float = Query(..., description="Land size in acres")
|
| 2000 |
+
):
|
| 2001 |
+
"""
|
| 2002 |
+
Get profit potential analysis based on current prices.
|
| 2003 |
+
Shows expected income per acre and profit potential.
|
| 2004 |
+
"""
|
| 2005 |
+
try:
|
| 2006 |
+
farmer_context = {"land_size": land_size}
|
| 2007 |
+
result = await run_market_advisory(crop, farmer_context)
|
| 2008 |
+
|
| 2009 |
+
return {
|
| 2010 |
+
"crop": crop,
|
| 2011 |
+
"land_size_acres": land_size,
|
| 2012 |
+
"current_price_pkr": result.get("current_price_pkr"),
|
| 2013 |
+
"expected_income_per_acre": result.get("expected_income_per_acre"),
|
| 2014 |
+
"profit_potential": result.get("profit_potential"),
|
| 2015 |
+
"price_forecast": result.get("price_forecast_simple"),
|
| 2016 |
+
"best_move": result.get("best_move")
|
| 2017 |
+
}
|
| 2018 |
+
except Exception as e:
|
| 2019 |
+
raise HTTPException(status_code=500, detail=f"Profit analysis error: {str(e)}")
|
| 2020 |
+
|
| 2021 |
+
@app.get("/market-advisory/risks", tags=["market-advisory"])
|
| 2022 |
+
async def get_market_risks(
|
| 2023 |
+
crop: str = Query(..., description="Crop name")
|
| 2024 |
+
):
|
| 2025 |
+
"""
|
| 2026 |
+
Get risk assessment and protection strategies.
|
| 2027 |
+
Warns farmers about potential problems and how to avoid them.
|
| 2028 |
+
"""
|
| 2029 |
+
try:
|
| 2030 |
+
result = await run_market_advisory(crop)
|
| 2031 |
+
|
| 2032 |
+
return {
|
| 2033 |
+
"crop": crop,
|
| 2034 |
+
"main_action": result.get("main_action"),
|
| 2035 |
+
"risks_to_watch": result.get("risks_to_watch", []),
|
| 2036 |
+
"how_to_protect": result.get("how_to_protect", []),
|
| 2037 |
+
"if_cant_sell_now": result.get("if_cant_sell_now", []),
|
| 2038 |
+
"storage_advice": result.get("storage_advice"),
|
| 2039 |
+
"confidence_level": result.get("confidence_level")
|
| 2040 |
+
}
|
| 2041 |
+
except Exception as e:
|
| 2042 |
+
raise HTTPException(status_code=500, detail=f"Risk assessment error: {str(e)}")
|
| 2043 |
+
|
| 2044 |
+
@app.get("/market-advisory/seasonal", tags=["market-advisory"])
|
| 2045 |
+
async def get_seasonal_market_advice(
|
| 2046 |
+
crop: str = Query(..., description="Crop name")
|
| 2047 |
+
):
|
| 2048 |
+
"""
|
| 2049 |
+
Get seasonal market advice and timing guidance.
|
| 2050 |
+
Explains how seasons affect prices and when to act.
|
| 2051 |
+
"""
|
| 2052 |
+
try:
|
| 2053 |
+
result = await run_market_advisory(crop)
|
| 2054 |
+
|
| 2055 |
+
return {
|
| 2056 |
+
"crop": crop,
|
| 2057 |
+
"seasonal_advice": result.get("seasonal_advice"),
|
| 2058 |
+
"upcoming_events": result.get("upcoming_events", []),
|
| 2059 |
+
"when_to_act": result.get("when_to_act"),
|
| 2060 |
+
"supply_situation": result.get("supply_situation"),
|
| 2061 |
+
"demand_situation": result.get("demand_situation"),
|
| 2062 |
+
"competition_level": result.get("competition_level")
|
| 2063 |
+
}
|
| 2064 |
+
except Exception as e:
|
| 2065 |
+
raise HTTPException(status_code=500, detail=f"Seasonal advice error: {str(e)}")
|
| 2066 |
+
|
| 2067 |
+
# ================== ADMIN ENDPOINTS ==================
|
| 2068 |
+
|
| 2069 |
+
@app.post("/admin/seed-entries", tags=["admin"])
|
| 2070 |
+
async def seed_sample_entries(count: int = Query(50, ge=10, le=200)):
|
| 2071 |
+
"""Seed sample ledger entries for testing"""
|
| 2072 |
+
try:
|
| 2073 |
+
import random
|
| 2074 |
+
from datetime import timedelta
|
| 2075 |
+
|
| 2076 |
+
categories = ["seeds", "fertilizer", "pesticide", "labor", "equipment", "crop_sale", "other"]
|
| 2077 |
+
payment_methods = ["cash", "bank_transfer", "mobile_money"]
|
| 2078 |
+
|
| 2079 |
+
ref = get_db_ref("entries")
|
| 2080 |
+
created_count = 0
|
| 2081 |
+
|
| 2082 |
+
for i in range(count):
|
| 2083 |
+
entry_type = random.choice(["income", "expense"])
|
| 2084 |
+
category = random.choice(categories)
|
| 2085 |
+
|
| 2086 |
+
# Generate realistic amounts
|
| 2087 |
+
if entry_type == "income":
|
| 2088 |
+
amount = round(random.uniform(5000, 50000), 2)
|
| 2089 |
+
else:
|
| 2090 |
+
amount = round(random.uniform(500, 15000), 2)
|
| 2091 |
+
|
| 2092 |
+
# Random date in last 60 days
|
| 2093 |
+
days_ago = random.randint(0, 60)
|
| 2094 |
+
created_at = (datetime.now() - timedelta(days=days_ago)).isoformat()
|
| 2095 |
+
|
| 2096 |
+
entry_id = generate_short_id(7)
|
| 2097 |
+
entry = {
|
| 2098 |
+
"type": "entry",
|
| 2099 |
+
"entryType": entry_type,
|
| 2100 |
+
"category": category,
|
| 2101 |
+
"amount": amount,
|
| 2102 |
+
"currency": "PKR",
|
| 2103 |
+
"paymentMethod": random.choice(payment_methods),
|
| 2104 |
+
"notes": f"Sample {entry_type} entry for testing",
|
| 2105 |
+
"createdAt": created_at,
|
| 2106 |
+
"recordedBy": "seed_script",
|
| 2107 |
+
"deviceId": "seeder",
|
| 2108 |
+
"syncStatus": "synced",
|
| 2109 |
+
"meta": None
|
| 2110 |
+
}
|
| 2111 |
+
|
| 2112 |
+
ref.child(entry_id).set(entry)
|
| 2113 |
+
created_count += 1
|
| 2114 |
+
|
| 2115 |
+
return {
|
| 2116 |
+
"status": "success",
|
| 2117 |
+
"message": f"Created {created_count} sample entries",
|
| 2118 |
+
"count": created_count
|
| 2119 |
+
}
|
| 2120 |
+
|
| 2121 |
+
except Exception as e:
|
| 2122 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 2123 |
+
|
| 2124 |
+
|
| 2125 |
+
@app.post("/admin/seed-market-data", response_model=InitializationResponse, tags=["admin"])
|
| 2126 |
+
async def seed_market_data():
|
| 2127 |
+
"""Seed 200 market price entries"""
|
| 2128 |
+
try:
|
| 2129 |
+
# Import and run the seed script
|
| 2130 |
+
from seed_market_data import seed_200_entries
|
| 2131 |
+
|
| 2132 |
+
entries = seed_200_entries()
|
| 2133 |
+
|
| 2134 |
+
return InitializationResponse(
|
| 2135 |
+
status="success",
|
| 2136 |
+
message=f"Successfully seeded {len(entries)} market price entries",
|
| 2137 |
+
records_created=len(entries),
|
| 2138 |
+
timestamp=datetime.utcnow().isoformat()
|
| 2139 |
+
)
|
| 2140 |
+
except Exception as e:
|
| 2141 |
+
raise HTTPException(status_code=500, detail=f"Seeding error: {str(e)}")
|
| 2142 |
+
|
| 2143 |
+
|
| 2144 |
+
@app.delete("/admin/clear-entries", tags=["admin"])
|
| 2145 |
+
async def clear_all_entries():
|
| 2146 |
+
"""Clear all ledger entries (use with caution!)"""
|
| 2147 |
+
try:
|
| 2148 |
+
ref = get_db_ref("entries")
|
| 2149 |
+
ref.delete()
|
| 2150 |
+
return {"message": "All entries cleared", "status": "success"}
|
| 2151 |
+
except Exception as e:
|
| 2152 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 2153 |
+
|
| 2154 |
+
|
| 2155 |
+
@app.delete("/admin/clear-market-data", tags=["admin"])
|
| 2156 |
+
async def clear_market_data():
|
| 2157 |
+
"""Clear all market data (use with caution!)"""
|
| 2158 |
+
try:
|
| 2159 |
+
ref = get_db_ref("market_data")
|
| 2160 |
+
ref.delete()
|
| 2161 |
+
return {"message": "All market data cleared", "status": "success"}
|
| 2162 |
+
except Exception as e:
|
| 2163 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 2164 |
+
|
| 2165 |
+
|
| 2166 |
+
# ================== SYSTEM ENDPOINTS ==================
|
| 2167 |
+
|
| 2168 |
+
@app.get("/health", response_model=HealthStatus, tags=["system"])
|
| 2169 |
+
async def health_check():
|
| 2170 |
+
"""Comprehensive health check"""
|
| 2171 |
+
try:
|
| 2172 |
+
# Check Firebase connection
|
| 2173 |
+
ref = get_db_ref("market_data/latest")
|
| 2174 |
+
latest = ref.get()
|
| 2175 |
+
|
| 2176 |
+
# Check entries
|
| 2177 |
+
entries_ref = get_db_ref("entries")
|
| 2178 |
+
entries = entries_ref.get() or {}
|
| 2179 |
+
|
| 2180 |
+
# Check market data
|
| 2181 |
+
prices_ref = get_db_ref("market_data/prices")
|
| 2182 |
+
prices = prices_ref.get() or {}
|
| 2183 |
+
|
| 2184 |
+
market_data_age = "unknown"
|
| 2185 |
+
if latest and "timestamp" in latest:
|
| 2186 |
+
try:
|
| 2187 |
+
ts = datetime.fromisoformat(latest["timestamp"].replace('Z', '+00:00'))
|
| 2188 |
+
age_hours = (datetime.utcnow() - ts).total_seconds() / 3600
|
| 2189 |
+
market_data_age = f"{age_hours:.1f} hours ago"
|
| 2190 |
+
except:
|
| 2191 |
+
pass
|
| 2192 |
+
|
| 2193 |
+
# Count historical records
|
| 2194 |
+
historical_count = 0
|
| 2195 |
+
for crop_data in prices.values():
|
| 2196 |
+
if isinstance(crop_data, dict):
|
| 2197 |
+
for date_data in crop_data.values():
|
| 2198 |
+
if isinstance(date_data, dict):
|
| 2199 |
+
historical_count += len(date_data)
|
| 2200 |
+
|
| 2201 |
+
return HealthStatus(
|
| 2202 |
+
status="healthy",
|
| 2203 |
+
timestamp=datetime.utcnow().isoformat(),
|
| 2204 |
+
services={
|
| 2205 |
+
"firebase": "connected",
|
| 2206 |
+
"market_agent": "active",
|
| 2207 |
+
"entries": f"{len(entries)} records"
|
| 2208 |
+
},
|
| 2209 |
+
market_data_age=market_data_age,
|
| 2210 |
+
historical_records=historical_count
|
| 2211 |
+
)
|
| 2212 |
+
except Exception as e:
|
| 2213 |
+
return HealthStatus(
|
| 2214 |
+
status="degraded",
|
| 2215 |
+
timestamp=datetime.utcnow().isoformat(),
|
| 2216 |
+
services={
|
| 2217 |
+
"firebase": f"error: {str(e)}",
|
| 2218 |
+
"market_agent": "unknown",
|
| 2219 |
+
"entries": "unknown"
|
| 2220 |
+
}
|
| 2221 |
+
)
|
| 2222 |
+
|
| 2223 |
+
|
| 2224 |
+
@app.get("/", tags=["system"])
|
| 2225 |
+
async def root():
|
| 2226 |
+
"""API information"""
|
| 2227 |
+
return {
|
| 2228 |
+
"name": "AgriLedger+ API",
|
| 2229 |
+
"version": "3.0.0",
|
| 2230 |
+
"description": "AI-powered agricultural ledger and market advisory system",
|
| 2231 |
+
"endpoints": {
|
| 2232 |
+
"entries": "/entries",
|
| 2233 |
+
"market_advice": "/advice/{crop}",
|
| 2234 |
+
"market_summary": "/advice/summary/all",
|
| 2235 |
+
"current_prices": "/market/prices/current",
|
| 2236 |
+
"price_history": "/market/prices/history/{crop}",
|
| 2237 |
+
"analytics": "/market/analytics/{crop}",
|
| 2238 |
+
"health": "/health",
|
| 2239 |
+
"docs": "/docs"
|
| 2240 |
+
},
|
| 2241 |
+
"supported_crops": ["wheat", "rice", "maize", "cotton", "sugarcane", "potato", "onion", "tomato"],
|
| 2242 |
+
"features": [
|
| 2243 |
+
"Voice-enabled ledger entries",
|
| 2244 |
+
"AI market advisory",
|
| 2245 |
+
"Historical price analysis",
|
| 2246 |
+
"Seasonal context",
|
| 2247 |
+
"Multi-crop support"
|
| 2248 |
+
],
|
| 2249 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 2250 |
+
}
|
| 2251 |
+
|
| 2252 |
+
|
| 2253 |
+
# ================== RUN APP ==================
|
| 2254 |
+
|
| 2255 |
+
def main():
|
| 2256 |
+
import uvicorn
|
| 2257 |
+
port = int(os.getenv("PORT", "8000"))
|
| 2258 |
+
uvicorn.run(app, host="0.0.0.0", port=port)
|
| 2259 |
+
|
| 2260 |
+
|
| 2261 |
+
if __name__ == "__main__":
|
| 2262 |
+
main()
|
| 2263 |
+
|
| 2264 |
+
handler = app
|
models/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .expense import EntryMeta, EntryCreate, EntryResponse
|
| 2 |
+
|
| 3 |
+
__all__ = [
|
| 4 |
+
"EntryMeta",
|
| 5 |
+
"EntryCreate",
|
| 6 |
+
"EntryResponse",
|
| 7 |
+
]
|
models/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (263 Bytes). View file
|
|
|
models/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (263 Bytes). View file
|
|
|
models/__pycache__/expense.cpython-312.pyc
ADDED
|
Binary file (1.89 kB). View file
|
|
|
models/__pycache__/expense.cpython-313.pyc
ADDED
|
Binary file (1.98 kB). View file
|
|
|
models/__pycache__/market.cpython-313.pyc
ADDED
|
Binary file (5.15 kB). View file
|
|
|
models/expense.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional
|
| 2 |
+
from pydantic import BaseModel, Field
|
| 3 |
+
|
| 4 |
+
class EntryMeta(BaseModel):
|
| 5 |
+
voiceText: Optional[str] = None
|
| 6 |
+
asrConfidence: Optional[float] = Field(default=None, ge=0, le=1)
|
| 7 |
+
lang: Optional[str] = None
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class EntryCreate(BaseModel):
|
| 11 |
+
entryType: str # "expense" or "income"
|
| 12 |
+
category: str
|
| 13 |
+
amount: float
|
| 14 |
+
date: Optional[str] = None # ISO string from client; falls back to server time
|
| 15 |
+
currency: str = Field(default="PKR", min_length=1)
|
| 16 |
+
paymentMethod: str = Field(default="cash")
|
| 17 |
+
notes: Optional[str] = None
|
| 18 |
+
recordedBy: str # <-- changed to free string (user id, username, etc.)
|
| 19 |
+
deviceId: Optional[str] = None
|
| 20 |
+
meta: Optional[EntryMeta] = None
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class EntryResponse(BaseModel):
|
| 24 |
+
id: str
|
| 25 |
+
type: str = Field(default="entry")
|
| 26 |
+
entryType: str
|
| 27 |
+
category: str
|
| 28 |
+
amount: float
|
| 29 |
+
currency: str
|
| 30 |
+
paymentMethod: str
|
| 31 |
+
notes: Optional[str]
|
| 32 |
+
createdAt: str
|
| 33 |
+
recordedBy: str # <-- free string allowed here too
|
| 34 |
+
deviceId: Optional[str]
|
| 35 |
+
syncStatus: str = Field(default="local")
|
| 36 |
+
meta: Optional[EntryMeta]
|
models/market.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
+
from typing import Optional, List, Dict, Any
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
|
| 5 |
+
# -------------------- Market Data Models --------------------
|
| 6 |
+
class MarketData(BaseModel):
|
| 7 |
+
crop: str
|
| 8 |
+
price: float
|
| 9 |
+
currency: str = "PKR"
|
| 10 |
+
timestamp: Optional[str] = None
|
| 11 |
+
|
| 12 |
+
class PriceTrend(BaseModel):
|
| 13 |
+
crop: str
|
| 14 |
+
trend: str # rising, falling, stable
|
| 15 |
+
percent_change: float
|
| 16 |
+
current_price: float
|
| 17 |
+
starting_price: float
|
| 18 |
+
volatility: str # low, moderate, high
|
| 19 |
+
avg_volatility: float
|
| 20 |
+
confidence: str # low, medium, high
|
| 21 |
+
data_points: int
|
| 22 |
+
analysis_period: str
|
| 23 |
+
price_history: Optional[List[float]] = None
|
| 24 |
+
|
| 25 |
+
class SeasonalContext(BaseModel):
|
| 26 |
+
crop: str
|
| 27 |
+
current_month: int
|
| 28 |
+
in_planting_season: bool
|
| 29 |
+
in_harvest_season: bool
|
| 30 |
+
months_to_planting: int
|
| 31 |
+
months_to_harvest: int
|
| 32 |
+
seasonal_advice: str
|
| 33 |
+
|
| 34 |
+
# -------------------- Advice Request/Response Models --------------------
|
| 35 |
+
class AdviceRequest(BaseModel):
|
| 36 |
+
crop: str
|
| 37 |
+
risk_tolerance: Optional[str] = Field(None, description="low, medium, or high")
|
| 38 |
+
budget: Optional[float] = Field(None, description="Available budget in PKR")
|
| 39 |
+
land_size: Optional[float] = Field(None, description="Land size in acres")
|
| 40 |
+
|
| 41 |
+
class MarketAdviceResponse(BaseModel):
|
| 42 |
+
crop: str
|
| 43 |
+
action: str # SELL NOW, BUY INPUTS, HOLD, WAIT
|
| 44 |
+
current_price: float
|
| 45 |
+
price_trend: str
|
| 46 |
+
confidence: str
|
| 47 |
+
reasoning: str
|
| 48 |
+
risk_factors: List[str]
|
| 49 |
+
seasonal_context: str
|
| 50 |
+
timestamp: str
|
| 51 |
+
raw_advice: str
|
| 52 |
+
|
| 53 |
+
class CropSummary(BaseModel):
|
| 54 |
+
crop: str
|
| 55 |
+
action: str
|
| 56 |
+
current_price: float
|
| 57 |
+
trend: str
|
| 58 |
+
confidence: str
|
| 59 |
+
quick_summary: str
|
| 60 |
+
|
| 61 |
+
class MarketSummaryResponse(BaseModel):
|
| 62 |
+
timestamp: str
|
| 63 |
+
farmer_context: Optional[Dict[str, Any]] = None
|
| 64 |
+
crops: List[CropSummary]
|
| 65 |
+
market_overview: str
|
| 66 |
+
|
| 67 |
+
# -------------------- Historical Data Models --------------------
|
| 68 |
+
class PricePoint(BaseModel):
|
| 69 |
+
timestamp: str
|
| 70 |
+
price: float
|
| 71 |
+
exchange_rate: Optional[float] = None
|
| 72 |
+
|
| 73 |
+
class HistoricalData(BaseModel):
|
| 74 |
+
crop: str
|
| 75 |
+
period: str
|
| 76 |
+
data_points: List[PricePoint]
|
| 77 |
+
|
| 78 |
+
class MarketAnalytics(BaseModel):
|
| 79 |
+
crop: str
|
| 80 |
+
period: str
|
| 81 |
+
avg_price: float
|
| 82 |
+
min_price: float
|
| 83 |
+
max_price: float
|
| 84 |
+
volatility_index: float
|
| 85 |
+
trend_direction: str
|
| 86 |
+
confidence_score: float
|
| 87 |
+
|
| 88 |
+
# -------------------- System Models --------------------
|
| 89 |
+
class HealthStatus(BaseModel):
|
| 90 |
+
status: str
|
| 91 |
+
timestamp: str
|
| 92 |
+
services: Dict[str, str]
|
| 93 |
+
market_data_age: Optional[str] = None
|
| 94 |
+
historical_records: Optional[int] = None
|
| 95 |
+
|
| 96 |
+
class InitializationResponse(BaseModel):
|
| 97 |
+
status: str
|
| 98 |
+
message: str
|
| 99 |
+
records_created: int
|
| 100 |
+
timestamp: str
|
requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
python-dotenv
|
| 3 |
+
httpx
|
| 4 |
+
uvicorn
|
| 5 |
+
bcrypt
|
| 6 |
+
pydantic
|
| 7 |
+
openai-agents
|
| 8 |
+
requests
|
| 9 |
+
firebase-admin
|
seed_market_data.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Seed 200 realistic market price entries into Firebase
|
| 3 |
+
Run this once to populate your database with test data
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import random
|
| 7 |
+
from datetime import datetime, timedelta
|
| 8 |
+
from firebase_config import get_db_ref
|
| 9 |
+
|
| 10 |
+
# Pakistan crop base prices (PKR per 40kg/maund)
|
| 11 |
+
CROP_BASE_PRICES = {
|
| 12 |
+
"wheat": 3500,
|
| 13 |
+
"rice": 5200,
|
| 14 |
+
"maize": 2800,
|
| 15 |
+
"cotton": 8500,
|
| 16 |
+
"sugarcane": 350,
|
| 17 |
+
"potato": 2200,
|
| 18 |
+
"onion": 1800,
|
| 19 |
+
"tomato": 3200,
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
# Major markets across Pakistan
|
| 23 |
+
MARKETS = {
|
| 24 |
+
"Sindh": ["Karachi", "Hyderabad", "Sukkur", "Larkana", "Mirpurkhas"],
|
| 25 |
+
"Punjab": ["Lahore", "Faisalabad", "Multan", "Rawalpindi", "Gujranwala"],
|
| 26 |
+
"Khyber Pakhtunkhwa": ["Peshawar", "Mardan", "Abbottabad", "Swat"],
|
| 27 |
+
"Balochistan": ["Quetta", "Khuzdar", "Turbat"]
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
# Regional price multipliers
|
| 31 |
+
REGIONAL_MULTIPLIERS = {
|
| 32 |
+
"Sindh": 1.02,
|
| 33 |
+
"Punjab": 1.0,
|
| 34 |
+
"Khyber Pakhtunkhwa": 1.05,
|
| 35 |
+
"Balochistan": 1.08,
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
# Seasonal trends (monthly multipliers)
|
| 39 |
+
SEASONAL_TRENDS = {
|
| 40 |
+
"wheat": {1: 1.05, 2: 1.08, 3: 1.10, 4: 0.95, 5: 0.92, 6: 0.95,
|
| 41 |
+
7: 0.98, 8: 1.0, 9: 1.02, 10: 1.05, 11: 1.03, 12: 1.02},
|
| 42 |
+
"rice": {1: 1.02, 2: 1.0, 3: 0.98, 4: 0.96, 5: 0.95, 6: 0.98,
|
| 43 |
+
7: 1.0, 8: 1.05, 9: 1.08, 10: 0.95, 11: 0.92, 12: 0.98},
|
| 44 |
+
"cotton": {1: 1.0, 2: 0.98, 3: 0.96, 4: 0.95, 5: 1.0, 6: 1.05,
|
| 45 |
+
7: 1.08, 8: 1.10, 9: 0.95, 10: 0.90, 11: 0.92, 12: 0.95},
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def generate_price_entry(crop, province, city, base_date, day_offset):
|
| 50 |
+
"""Generate a single realistic price entry"""
|
| 51 |
+
|
| 52 |
+
base_price = CROP_BASE_PRICES.get(crop, 3000)
|
| 53 |
+
regional_mult = REGIONAL_MULTIPLIERS.get(province, 1.0)
|
| 54 |
+
|
| 55 |
+
# Calculate date
|
| 56 |
+
entry_date = base_date - timedelta(days=day_offset)
|
| 57 |
+
month = entry_date.month
|
| 58 |
+
|
| 59 |
+
# Get seasonal multiplier
|
| 60 |
+
seasonal_mult = SEASONAL_TRENDS.get(crop, {}).get(month, 1.0)
|
| 61 |
+
|
| 62 |
+
# Add realistic daily variation
|
| 63 |
+
daily_variation = random.uniform(-0.03, 0.03) # ยฑ3% daily
|
| 64 |
+
|
| 65 |
+
# Add trend over time (slight price increase over 60 days)
|
| 66 |
+
trend_mult = 1 + (day_offset / 60) * 0.05 # 5% increase over period
|
| 67 |
+
|
| 68 |
+
# Calculate final price
|
| 69 |
+
modal_price = base_price * regional_mult * seasonal_mult * (1 + daily_variation) * trend_mult
|
| 70 |
+
|
| 71 |
+
# Generate min/max prices (ยฑ5% of modal)
|
| 72 |
+
min_price = modal_price * random.uniform(0.95, 0.98)
|
| 73 |
+
max_price = modal_price * random.uniform(1.02, 1.05)
|
| 74 |
+
|
| 75 |
+
return {
|
| 76 |
+
"crop": crop,
|
| 77 |
+
"market": f"{city} Mandi",
|
| 78 |
+
"province": province,
|
| 79 |
+
"city": city,
|
| 80 |
+
"min_price": round(min_price, 2),
|
| 81 |
+
"max_price": round(max_price, 2),
|
| 82 |
+
"modal_price": round(modal_price, 2),
|
| 83 |
+
"currency": "PKR",
|
| 84 |
+
"unit": "40kg",
|
| 85 |
+
"timestamp": entry_date.isoformat(),
|
| 86 |
+
"date_key": entry_date.strftime("%Y-%m-%d"),
|
| 87 |
+
"source": "seeded"
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def seed_200_entries():
|
| 92 |
+
"""Seed exactly 200 market price entries"""
|
| 93 |
+
|
| 94 |
+
print("Starting to seed 200 market price entries...")
|
| 95 |
+
print("=" * 60)
|
| 96 |
+
|
| 97 |
+
ref = get_db_ref("market_data/prices")
|
| 98 |
+
base_date = datetime.now()
|
| 99 |
+
|
| 100 |
+
entries = []
|
| 101 |
+
|
| 102 |
+
# Strategy: 200 entries over 60 days across different crops and markets
|
| 103 |
+
# 8 crops ร 25 entries each = 200 total
|
| 104 |
+
|
| 105 |
+
crops_to_seed = list(CROP_BASE_PRICES.keys())
|
| 106 |
+
entries_per_crop = 200 // len(crops_to_seed) # 25 entries per crop
|
| 107 |
+
|
| 108 |
+
entry_count = 0
|
| 109 |
+
|
| 110 |
+
for crop in crops_to_seed:
|
| 111 |
+
print(f"\nSeeding {crop}...")
|
| 112 |
+
|
| 113 |
+
# Distribute entries over 60 days
|
| 114 |
+
for i in range(entries_per_crop):
|
| 115 |
+
day_offset = (i * 60) // entries_per_crop # Spread across 60 days
|
| 116 |
+
|
| 117 |
+
# Rotate through provinces and cities
|
| 118 |
+
province = random.choice(list(MARKETS.keys()))
|
| 119 |
+
city = random.choice(MARKETS[province])
|
| 120 |
+
|
| 121 |
+
entry = generate_price_entry(crop, province, city, base_date, day_offset)
|
| 122 |
+
|
| 123 |
+
# Store in Firebase
|
| 124 |
+
# Path: market_data/prices/{crop}/{date_key}/{push_id}
|
| 125 |
+
date_key = entry["date_key"]
|
| 126 |
+
ref.child(f"{crop}/{date_key}").push(entry)
|
| 127 |
+
|
| 128 |
+
entries.append(entry)
|
| 129 |
+
entry_count += 1
|
| 130 |
+
|
| 131 |
+
if entry_count % 25 == 0:
|
| 132 |
+
print(f" Progress: {entry_count}/200 entries created")
|
| 133 |
+
|
| 134 |
+
print(f"\n{'=' * 60}")
|
| 135 |
+
print(f"โ
Successfully seeded {entry_count} market price entries!")
|
| 136 |
+
print(f"\nBreakdown:")
|
| 137 |
+
|
| 138 |
+
# Show breakdown by crop
|
| 139 |
+
for crop in crops_to_seed:
|
| 140 |
+
crop_entries = [e for e in entries if e["crop"] == crop]
|
| 141 |
+
if crop_entries:
|
| 142 |
+
avg_price = sum(e["modal_price"] for e in crop_entries) / len(crop_entries)
|
| 143 |
+
print(f" โข {crop}: {len(crop_entries)} entries, avg price: โจ{avg_price:,.2f}")
|
| 144 |
+
|
| 145 |
+
# Create latest prices snapshot
|
| 146 |
+
print("\nCreating latest prices snapshot...")
|
| 147 |
+
latest_prices = {}
|
| 148 |
+
for crop in crops_to_seed:
|
| 149 |
+
crop_entries = [e for e in entries if e["crop"] == crop]
|
| 150 |
+
if crop_entries:
|
| 151 |
+
latest = max(crop_entries, key=lambda x: x["timestamp"])
|
| 152 |
+
latest_prices[crop] = latest["modal_price"]
|
| 153 |
+
|
| 154 |
+
latest_ref = get_db_ref("market_data/latest")
|
| 155 |
+
latest_ref.set({
|
| 156 |
+
"prices": latest_prices,
|
| 157 |
+
"timestamp": datetime.now().isoformat(),
|
| 158 |
+
"status": "seeded",
|
| 159 |
+
"source": "seed_script",
|
| 160 |
+
"total_crops": len(latest_prices)
|
| 161 |
+
})
|
| 162 |
+
|
| 163 |
+
print("โ
Latest prices snapshot created")
|
| 164 |
+
|
| 165 |
+
# Create metadata
|
| 166 |
+
metadata_ref = get_db_ref("market_data/metadata")
|
| 167 |
+
metadata_ref.set({
|
| 168 |
+
"total_entries": entry_count,
|
| 169 |
+
"crops": crops_to_seed,
|
| 170 |
+
"provinces": list(MARKETS.keys()),
|
| 171 |
+
"date_range": {
|
| 172 |
+
"start": (base_date - timedelta(days=60)).strftime("%Y-%m-%d"),
|
| 173 |
+
"end": base_date.strftime("%Y-%m-%d")
|
| 174 |
+
},
|
| 175 |
+
"last_seeded": datetime.now().isoformat(),
|
| 176 |
+
"currency": "PKR",
|
| 177 |
+
"unit": "40kg (Maund)"
|
| 178 |
+
})
|
| 179 |
+
|
| 180 |
+
print("โ
Metadata created")
|
| 181 |
+
print(f"\n{'=' * 60}")
|
| 182 |
+
print("๐ Database seeding complete!")
|
| 183 |
+
print("\nYou can now use the Market Agent to get advice based on this data.")
|
| 184 |
+
|
| 185 |
+
return entries
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def verify_seeded_data():
|
| 189 |
+
"""Verify that data was seeded correctly"""
|
| 190 |
+
print("\n" + "=" * 60)
|
| 191 |
+
print("Verifying seeded data...")
|
| 192 |
+
print("=" * 60)
|
| 193 |
+
|
| 194 |
+
ref = get_db_ref("market_data/prices")
|
| 195 |
+
all_data = ref.get() or {}
|
| 196 |
+
|
| 197 |
+
total_entries = 0
|
| 198 |
+
for crop, dates in all_data.items():
|
| 199 |
+
if isinstance(dates, dict):
|
| 200 |
+
for date, entries in dates.items():
|
| 201 |
+
if isinstance(entries, dict):
|
| 202 |
+
total_entries += len(entries)
|
| 203 |
+
|
| 204 |
+
print(f"\n๐ Total entries in database: {total_entries}")
|
| 205 |
+
|
| 206 |
+
# Check latest prices
|
| 207 |
+
latest_ref = get_db_ref("market_data/latest")
|
| 208 |
+
latest = latest_ref.get()
|
| 209 |
+
|
| 210 |
+
if latest:
|
| 211 |
+
print(f"\n๐น Latest prices available:")
|
| 212 |
+
for crop, price in latest.get("prices", {}).items():
|
| 213 |
+
print(f" โข {crop}: โจ{price:,.2f}")
|
| 214 |
+
|
| 215 |
+
# Check metadata
|
| 216 |
+
metadata_ref = get_db_ref("market_data/metadata")
|
| 217 |
+
metadata = metadata_ref.get()
|
| 218 |
+
|
| 219 |
+
if metadata:
|
| 220 |
+
print(f"\n๐ Metadata:")
|
| 221 |
+
print(f" โข Date range: {metadata.get('date_range', {}).get('start')} to {metadata.get('date_range', {}).get('end')}")
|
| 222 |
+
print(f" โข Crops: {len(metadata.get('crops', []))}")
|
| 223 |
+
print(f" โข Provinces: {len(metadata.get('provinces', []))}")
|
| 224 |
+
|
| 225 |
+
print(f"\n{'=' * 60}")
|
| 226 |
+
|
| 227 |
+
if total_entries >= 200:
|
| 228 |
+
print("โ
Verification PASSED - Database properly seeded!")
|
| 229 |
+
else:
|
| 230 |
+
print(f"โ ๏ธ Warning: Expected 200+ entries, found {total_entries}")
|
| 231 |
+
|
| 232 |
+
return total_entries
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
if __name__ == "__main__":
|
| 236 |
+
try:
|
| 237 |
+
# Seed the data
|
| 238 |
+
entries = seed_200_entries()
|
| 239 |
+
|
| 240 |
+
# Verify it worked
|
| 241 |
+
verify_seeded_data()
|
| 242 |
+
|
| 243 |
+
print("\nโจ Run your Market Agent now to get advice!")
|
| 244 |
+
|
| 245 |
+
except Exception as e:
|
| 246 |
+
print(f"\nโ Error during seeding: {e}")
|
| 247 |
+
import traceback
|
| 248 |
+
traceback.print_exc()
|
soil_data_seeder.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Seed 200 realistic soil condition entries into Firebase
|
| 3 |
+
Run this once to populate your database with test data for the Weather Agent
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import random
|
| 7 |
+
from datetime import datetime, timedelta
|
| 8 |
+
from firebase_config import get_db_ref
|
| 9 |
+
|
| 10 |
+
# ------------------------------------------------------
|
| 11 |
+
# ๐พ Base Soil Profiles (ideal conditions per crop)
|
| 12 |
+
# ------------------------------------------------------
|
| 13 |
+
SOIL_PROFILES = {
|
| 14 |
+
"wheat": {"moisture": (25, 35), "temp": (15, 25), "ph": (6.0, 7.5)},
|
| 15 |
+
"rice": {"moisture": (60, 80), "temp": (25, 35), "ph": (5.5, 6.5)},
|
| 16 |
+
"maize": {"moisture": (35, 50), "temp": (20, 30), "ph": (5.8, 7.0)},
|
| 17 |
+
"cotton": {"moisture": (30, 40), "temp": (22, 32), "ph": (6.0, 7.8)},
|
| 18 |
+
"sugarcane": {"moisture": (50, 70), "temp": (20, 30), "ph": (6.5, 7.5)},
|
| 19 |
+
"potato": {"moisture": (60, 75), "temp": (18, 25), "ph": (5.0, 6.0)},
|
| 20 |
+
"onion": {"moisture": (40, 60), "temp": (20, 30), "ph": (6.0, 7.0)},
|
| 21 |
+
"tomato": {"moisture": (50, 70), "temp": (20, 28), "ph": (5.5, 7.5)},
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
# ------------------------------------------------------
|
| 25 |
+
# ๐๏ธ Major provinces and regions
|
| 26 |
+
# ------------------------------------------------------
|
| 27 |
+
REGIONS = {
|
| 28 |
+
"Sindh": ["Karachi", "Hyderabad", "Sukkur", "Larkana"],
|
| 29 |
+
"Punjab": ["Lahore", "Multan", "Faisalabad", "Rawalpindi"],
|
| 30 |
+
"Khyber Pakhtunkhwa": ["Peshawar", "Mardan", "Swat"],
|
| 31 |
+
"Balochistan": ["Quetta", "Khuzdar", "Turbat"],
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
# ------------------------------------------------------
|
| 35 |
+
# ๐ก๏ธ Seasonal multipliers (approx. soil condition variations)
|
| 36 |
+
# ------------------------------------------------------
|
| 37 |
+
SEASONAL_EFFECTS = {
|
| 38 |
+
"winter": {"temp": 0.9, "moisture": 1.1},
|
| 39 |
+
"summer": {"temp": 1.1, "moisture": 0.9},
|
| 40 |
+
"monsoon": {"temp": 1.0, "moisture": 1.2},
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
# ------------------------------------------------------
|
| 44 |
+
# ๐งฎ Generate a single soil record
|
| 45 |
+
# ------------------------------------------------------
|
| 46 |
+
def generate_soil_entry(crop, province, city, base_date, day_offset):
|
| 47 |
+
profile = SOIL_PROFILES[crop]
|
| 48 |
+
entry_date = base_date - timedelta(days=day_offset)
|
| 49 |
+
month = entry_date.month
|
| 50 |
+
|
| 51 |
+
# Determine season
|
| 52 |
+
if month in [12, 1, 2]:
|
| 53 |
+
season = "winter"
|
| 54 |
+
elif month in [6, 7, 8]:
|
| 55 |
+
season = "monsoon"
|
| 56 |
+
else:
|
| 57 |
+
season = "summer"
|
| 58 |
+
|
| 59 |
+
seasonal = SEASONAL_EFFECTS.get(season, {"temp": 1.0, "moisture": 1.0})
|
| 60 |
+
|
| 61 |
+
# Apply multipliers + random variation
|
| 62 |
+
soil_temp = random.uniform(*profile["temp"]) * random.uniform(0.95, 1.05) * seasonal["temp"]
|
| 63 |
+
soil_moisture = random.uniform(*profile["moisture"]) * random.uniform(0.9, 1.1) * seasonal["moisture"]
|
| 64 |
+
ph = random.uniform(*profile["ph"]) * random.uniform(0.98, 1.02)
|
| 65 |
+
|
| 66 |
+
return {
|
| 67 |
+
"crop": crop,
|
| 68 |
+
"province": province,
|
| 69 |
+
"city": city,
|
| 70 |
+
"region": f"{city}, {province}",
|
| 71 |
+
"soil_temperature": round(soil_temp, 2),
|
| 72 |
+
"soil_moisture": round(soil_moisture, 2),
|
| 73 |
+
"ph_level": round(ph, 2),
|
| 74 |
+
"nitrogen": random.randint(10, 60),
|
| 75 |
+
"phosphorus": random.randint(5, 40),
|
| 76 |
+
"potassium": random.randint(10, 50),
|
| 77 |
+
"rainfall_mm": round(random.uniform(0, 50), 2),
|
| 78 |
+
"season": season,
|
| 79 |
+
"timestamp": entry_date.isoformat(),
|
| 80 |
+
"date_key": entry_date.strftime("%Y-%m-%d"),
|
| 81 |
+
"source": "seeded"
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
# ------------------------------------------------------
|
| 85 |
+
# ๐ฑ Seed 200 soil entries into Firebase
|
| 86 |
+
# ------------------------------------------------------
|
| 87 |
+
def seed_200_soil_entries():
|
| 88 |
+
print("Starting to seed 200 soil condition entries...")
|
| 89 |
+
print("=" * 60)
|
| 90 |
+
|
| 91 |
+
ref = get_db_ref("soil_data/records")
|
| 92 |
+
base_date = datetime.now()
|
| 93 |
+
entries = []
|
| 94 |
+
|
| 95 |
+
crops_to_seed = list(SOIL_PROFILES.keys())
|
| 96 |
+
entries_per_crop = 200 // len(crops_to_seed) # ~25 entries per crop
|
| 97 |
+
entry_count = 0
|
| 98 |
+
|
| 99 |
+
for crop in crops_to_seed:
|
| 100 |
+
print(f"\nSeeding soil data for {crop}...")
|
| 101 |
+
|
| 102 |
+
for i in range(entries_per_crop):
|
| 103 |
+
day_offset = (i * 60) // entries_per_crop
|
| 104 |
+
province = random.choice(list(REGIONS.keys()))
|
| 105 |
+
city = random.choice(REGIONS[province])
|
| 106 |
+
|
| 107 |
+
entry = generate_soil_entry(crop, province, city, base_date, day_offset)
|
| 108 |
+
date_key = entry["date_key"]
|
| 109 |
+
ref.child(f"{crop}/{date_key}").push(entry)
|
| 110 |
+
|
| 111 |
+
entries.append(entry)
|
| 112 |
+
entry_count += 1
|
| 113 |
+
|
| 114 |
+
if entry_count % 25 == 0:
|
| 115 |
+
print(f" Progress: {entry_count}/200 entries created")
|
| 116 |
+
|
| 117 |
+
print(f"\n{'=' * 60}")
|
| 118 |
+
print(f"โ
Successfully seeded {entry_count} soil condition entries!")
|
| 119 |
+
create_summary(entries)
|
| 120 |
+
create_latest_snapshot(entries)
|
| 121 |
+
create_metadata(entries, crops_to_seed)
|
| 122 |
+
return entries
|
| 123 |
+
|
| 124 |
+
# ------------------------------------------------------
|
| 125 |
+
# ๐ Summary, Latest Snapshot & Metadata
|
| 126 |
+
# ------------------------------------------------------
|
| 127 |
+
def create_summary(entries):
|
| 128 |
+
summary_ref = get_db_ref("soil_data/summary")
|
| 129 |
+
|
| 130 |
+
avg_temp = sum(e["soil_temperature"] for e in entries) / len(entries)
|
| 131 |
+
avg_moisture = sum(e["soil_moisture"] for e in entries) / len(entries)
|
| 132 |
+
avg_ph = sum(e["ph_level"] for e in entries) / len(entries)
|
| 133 |
+
|
| 134 |
+
summary = {
|
| 135 |
+
"total_entries": len(entries),
|
| 136 |
+
"average_temp": round(avg_temp, 2),
|
| 137 |
+
"average_moisture": round(avg_moisture, 2),
|
| 138 |
+
"average_ph": round(avg_ph, 2),
|
| 139 |
+
"last_updated": datetime.now().isoformat()
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
summary_ref.set(summary)
|
| 143 |
+
print("โ
Soil data summary uploaded!")
|
| 144 |
+
|
| 145 |
+
def create_latest_snapshot(entries):
|
| 146 |
+
latest_ref = get_db_ref("soil_data/latest")
|
| 147 |
+
latest_per_crop = {}
|
| 148 |
+
|
| 149 |
+
for crop in SOIL_PROFILES:
|
| 150 |
+
crop_entries = [e for e in entries if e["crop"] == crop]
|
| 151 |
+
if crop_entries:
|
| 152 |
+
latest = max(crop_entries, key=lambda x: x["timestamp"])
|
| 153 |
+
latest_per_crop[crop] = {
|
| 154 |
+
"soil_temperature": latest["soil_temperature"],
|
| 155 |
+
"soil_moisture": latest["soil_moisture"],
|
| 156 |
+
"ph_level": latest["ph_level"],
|
| 157 |
+
"region": latest["region"]
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
latest_ref.set({
|
| 161 |
+
"latest_conditions": latest_per_crop,
|
| 162 |
+
"timestamp": datetime.now().isoformat(),
|
| 163 |
+
"status": "seeded"
|
| 164 |
+
})
|
| 165 |
+
print("โ
Latest soil conditions snapshot created!")
|
| 166 |
+
|
| 167 |
+
def create_metadata(entries, crops):
|
| 168 |
+
metadata_ref = get_db_ref("soil_data/metadata")
|
| 169 |
+
metadata_ref.set({
|
| 170 |
+
"total_entries": len(entries),
|
| 171 |
+
"crops": crops,
|
| 172 |
+
"provinces": list(REGIONS.keys()),
|
| 173 |
+
"date_range": {
|
| 174 |
+
"start": min(e["date_key"] for e in entries),
|
| 175 |
+
"end": max(e["date_key"] for e in entries)
|
| 176 |
+
},
|
| 177 |
+
"last_seeded": datetime.now().isoformat(),
|
| 178 |
+
"unit": "Soil metrics (%) / ยฐC / pH"
|
| 179 |
+
})
|
| 180 |
+
print("โ
Metadata uploaded!")
|
| 181 |
+
|
| 182 |
+
# ------------------------------------------------------
|
| 183 |
+
# ๐ Verification
|
| 184 |
+
# ------------------------------------------------------
|
| 185 |
+
def verify_soil_data():
|
| 186 |
+
print("\n" + "=" * 60)
|
| 187 |
+
print("Verifying seeded soil data...")
|
| 188 |
+
print("=" * 60)
|
| 189 |
+
|
| 190 |
+
ref = get_db_ref("soil_data/records")
|
| 191 |
+
all_data = ref.get() or {}
|
| 192 |
+
total_entries = 0
|
| 193 |
+
|
| 194 |
+
for crop, dates in all_data.items():
|
| 195 |
+
if isinstance(dates, dict):
|
| 196 |
+
for date, records in dates.items():
|
| 197 |
+
if isinstance(records, dict):
|
| 198 |
+
total_entries += len(records)
|
| 199 |
+
|
| 200 |
+
print(f"\n๐ Total soil entries in database: {total_entries}")
|
| 201 |
+
|
| 202 |
+
summary_ref = get_db_ref("soil_data/summary")
|
| 203 |
+
summary = summary_ref.get()
|
| 204 |
+
if summary:
|
| 205 |
+
print(f"\n๐ก๏ธ Average Temp: {summary['average_temp']}ยฐC | ๐ง Moisture: {summary['average_moisture']}% | โ๏ธ pH: {summary['average_ph']}")
|
| 206 |
+
|
| 207 |
+
if total_entries >= 200:
|
| 208 |
+
print("โ
Verification PASSED - Database properly seeded!")
|
| 209 |
+
else:
|
| 210 |
+
print(f"โ ๏ธ Warning: Expected 200+ entries, found {total_entries}")
|
| 211 |
+
|
| 212 |
+
print(f"{'=' * 60}")
|
| 213 |
+
|
| 214 |
+
# ------------------------------------------------------
|
| 215 |
+
# ๐ Main Run
|
| 216 |
+
# ------------------------------------------------------
|
| 217 |
+
if __name__ == "__main__":
|
| 218 |
+
try:
|
| 219 |
+
entries = seed_200_soil_entries()
|
| 220 |
+
verify_soil_data()
|
| 221 |
+
print("\nโจ Soil Data Seeder complete! You can now connect it to your Weather Agent.")
|
| 222 |
+
except Exception as e:
|
| 223 |
+
print(f"\nโ Error during seeding: {e}")
|
| 224 |
+
import traceback
|
| 225 |
+
traceback.print_exc()
|