AI Recommendations API¶
AI-powered, context-aware recommendations using Retrieval-Augmented Generation (RAG). The system combines block-level data with policy documents (PDFs in ai-docs/) to generate actionable recommendations for a specific intervention.
The pipeline uses LangChain + OpenAI (gpt-4o-mini) with a Chroma vector store persisted at data/vectorstore/. Policy PDFs are embedded with OpenAIEmbeddings and retrieved by similarity search (top k=5 chunks).
Prerequisites
The AI recommendation feature requires:
OPENAI_API_KEYenvironment variable set on the server- RAG dependencies installed (
langchain,langchain-openai,langchain-chroma,langchain-community,chromadb,pypdf) - At least one PDF policy document placed in the
ai-docs/directory
The vector store is built lazily on the first POST /api/ai-recommendation
call (or you can trigger it explicitly with POST /api/ai-recommendation/init).
All other LEAF DSS features work without these prerequisites — when they are
missing, the AI endpoints degrade gracefully (HTTP 503) rather than breaking
the rest of the app.
Architecture¶
User Request
↓
Block Data (name, district, intervention, feasibility_score, metrics, filters)
↓
Chroma Vector Store ← PDF Policy Documents (ai-docs/) [OpenAIEmbeddings]
↓
Top k=5 relevant policy chunks retrieved (similarity search)
↓
OpenAI gpt-4o-mini generates recommendation with retrieved context
↓
Response: recommendation text + source PDF filenames
The RAG pipeline:
- Ingestion — PDFs in
ai-docs/are loaded withPyPDFLoader, split into overlapping chunks (chunk_size=1000,chunk_overlap=200) and embedded into Chroma usingOpenAIEmbeddings. This happens once and is persisted todata/vectorstore/. - Retrieval — When a recommendation is requested, the system builds a context query from the intervention plus the passing/failing metrics and queries Chroma for the top
k=5chunks. - Generation — Retrieved context + block data are sent to
gpt-4o-mini, which generates a tailored recommendation (kept under ~300 words). - Citation — The unique source PDF filenames behind the retrieved chunks are returned in
sources.
Livestock query enrichment (LEAF-57)
When the intervention is a livestock commodity (Dairy, Goatery, Piggery,
Backyard_Poultry, Duckery, Fishery_Activity) or the parent Livestock
category, the retrieval query is enriched with animal-husbandry vocabulary
(veterinary access, fodder, milk collection, Pashu Sakhi, DAY-NRLM livestock
convergence) so livestock policy chunks surface reliably.
Get AI Recommendation¶
Generates a context-aware recommendation for a specific block and intervention. The recommendation considers the block's variable values, which criteria are met vs. not met, and relevant policy-document context.
Request Body¶
{
"block_name": "Digboi",
"district_name": "Tinsukia",
"intervention": "Dairy",
"feasibility_score": 65.5,
"metrics": [
{
"label": "Agricultural Land %",
"value": 45.2,
"in_range": true,
"min": 30,
"max": 70
},
{
"label": "Water Availability",
"value": 15.0,
"in_range": false,
"min": 20,
"max": 50
}
],
"filters": [
{
"column": "AD",
"min_val": 30,
"max_val": 70,
"weight": 1.0
}
]
}
| Field | Type | Required | Description |
|---|---|---|---|
block_name |
string | No | Block name (defaults to "Unknown") |
district_name |
string | No | District name (defaults to "Unknown") |
intervention |
string | Yes | Selected intervention name. If empty/omitted, the endpoint returns 200 with a prompt to select one (no LLM call). |
feasibility_score |
number | No | Current feasibility score, 0–100 (defaults to 0) |
metrics |
array | No | Variable metrics with in-range status — tells the AI which criteria pass/fail (defaults to []) |
metrics[].label |
string | - | Human-readable variable name |
metrics[].value |
number | - | Current value for this block |
metrics[].in_range |
boolean | - | Whether the value falls in the acceptable range |
metrics[].min |
number | - | Minimum threshold |
metrics[].max |
number | - | Maximum threshold |
filters |
array | No | Active filter configurations (accepted and passed through; defaults to []) |
Response¶
{
"recommendation": "Digboi block shows moderate readiness (65.5%) for Dairy. Agricultural land (45.2%) is within range, but water availability (15.0%) is below the 20% minimum. Priority actions: 1) Invest in water-harvesting structures... 2) Strengthen fodder cultivation and veterinary access...",
"sources": [
"Assam_Dairy_Policy_2023.pdf",
"NABARD_Livestock_Guidelines.pdf"
],
"feasibility_score": 65.5,
"metrics_analyzed": 2,
"gaps_identified": 1,
"retrieved_context": [
{
"content": "…chunk text used as context…",
"source": "Assam_Dairy_Policy_2023.pdf"
}
]
}
| Field | Type | Description |
|---|---|---|
recommendation |
string | Generated recommendation text |
sources |
array of strings | Unique PDF filenames behind the retrieved context. Note: these are filenames only — the API does not return page numbers or per-source relevance scores. |
feasibility_score |
number | Echoes the submitted feasibility score |
metrics_analyzed |
integer | Count of metrics received |
gaps_identified |
integer | Count of metrics with in_range: false |
retrieved_context |
array | The raw retrieved chunks used for generation (content + source), included for verification/debugging |
Graceful degradation
When RAG is unavailable (missing dependencies or OPENAI_API_KEY), the endpoint
still returns a JSON body with a recommendation message, an error field, and
an empty sources array — with HTTP status 503. When no intervention is supplied
it returns 200 with a "Please select an intervention" message.
Errors¶
| Code | Description |
|---|---|
503 |
RAG service unavailable — dependencies not installed or OPENAI_API_KEY missing. Body still includes recommendation, error, and sources: []. |
500 |
Processing error during recommendation generation. Body includes recommendation (error message), error, and sources: []. |
Example¶
curl -X POST https://leaf-asrlm.in/api/ai-recommendation \
-H "Content-Type: application/json" \
-d '{
"block_name": "Digboi",
"district_name": "Tinsukia",
"intervention": "Dairy",
"feasibility_score": 65.5,
"metrics": [
{"label": "Agricultural Land %", "value": 45.2, "in_range": true, "min": 30, "max": 70},
{"label": "Water Availability", "value": 15.0, "in_range": false, "min": 20, "max": 50}
]
}'
import requests
response = requests.post(
"https://leaf-asrlm.in/api/ai-recommendation",
json={
"block_name": "Digboi",
"district_name": "Tinsukia",
"intervention": "Dairy",
"feasibility_score": 65.5,
"metrics": [
{"label": "Agricultural Land %", "value": 45.2,
"in_range": True, "min": 30, "max": 70},
{"label": "Water Availability", "value": 15.0,
"in_range": False, "min": 20, "max": 50}
]
}
)
data = response.json()
if response.status_code == 200:
print("Recommendation:\n", data["recommendation"])
print("\nSources:")
for src in data.get("sources", []):
print(f" - {src}")
else:
print(f"Error {response.status_code}: {data.get('error')}")
const res = await fetch("https://leaf-asrlm.in/api/ai-recommendation", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
block_name: "Digboi",
district_name: "Tinsukia",
intervention: "Dairy",
feasibility_score: 65.5,
metrics: [
{ label: "Agricultural Land %", value: 45.2, in_range: true, min: 30, max: 70 },
{ label: "Water Availability", value: 15.0, in_range: false, min: 20, max: 50 }
]
})
});
const data = await res.json();
console.log(data.recommendation);
console.log("Sources:", data.sources);
Initialize / Rebuild Vector Store¶
Loads the RAG vector store, building it from the PDFs in ai-docs/ if it does not already exist on disk. Because generation builds the store lazily on first use, calling this endpoint is optional — it lets you warm the cache (and surface any embedding errors) ahead of the first recommendation request.
Request Body¶
No body required.
Response¶
| Field | Type | Description |
|---|---|---|
success |
boolean | true if the vector store loaded/built successfully; false on failure (with an error field). |
Errors¶
| Code | Response | Description |
|---|---|---|
503 |
{"success": false, "error": "..."} |
RAG dependencies / OPENAI_API_KEY not available |
500 |
{"success": false, "error": "..."} |
Error while loading or building the vector store |
Example¶
When to force a rebuild
To pick up newly added or changed PDFs, delete the persisted store
(data/vectorstore/) so the next call re-embeds — or use the
Upload AI Document endpoint below, which wipes the
persisted store for you. The first rebuild takes roughly 30–120 seconds
depending on the number and size of PDFs.
Upload AI Document¶
Adds a single PDF to the AI knowledge base. The file is saved into the ai-docs/ retrieval pool and the persisted vector store is deleted, so the next POST /api/ai-recommendation call lazily rebuilds the embeddings to include the new document. Re-embedding is deferred to that next call, so this endpoint returns immediately.
Admin only — gated by the standard admin guard (?admin=1 query parameter or X-Admin: 1 header). There is no other authentication.
Request¶
multipart/form-data with a single file field named file. The file must be a .pdf.
| Field | Type | Required | Description |
|---|---|---|---|
file |
file (PDF) | Yes | The PDF document to add to the knowledge base. Non-PDF files are rejected with 400. |
Response¶
{
"ok": true,
"filename": "Assam_Dairy_Policy_2023.pdf",
"message": "Document added. AI knowledge base will rebuild on the next recommendation request."
}
| Field | Type | Description |
|---|---|---|
ok |
boolean | true when the upload was accepted |
filename |
string | The sanitized name the file was saved under (werkzeug.secure_filename) |
message |
string | Human-readable status note |
Errors¶
| Code | Description |
|---|---|
400 |
No file provided, no file selected, or the file is not a .pdf |
403 |
Caller is not an admin (missing ?admin=1 / X-Admin: 1) |
500 |
Server error while saving the document |