Tahasaif3 commited on
Commit
a7f2c72
ยท
1 Parent(s): d3f28f7

code pushed'

Browse files
.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()