Skip to content

Commit 6baf48f

Browse files
Merge pull request #13 from PolicyEngine/bot/issue-11
Improve MCP workflow for policy analysis
2 parents 521b3f9 + 5a5d072 commit 6baf48f

5 files changed

Lines changed: 136 additions & 9 deletions

File tree

src/policyengine_api/api/analysis.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66
77
This is an async operation - the endpoint returns immediately with a report_id,
88
and you poll /analysis/economic-impact/{report_id} until status is "completed".
9+
10+
WORKFLOW for full economic analysis:
11+
1. Create a policy with parameter changes: POST /policies
12+
2. Get a dataset: GET /datasets (look for UK/US datasets)
13+
3. Start analysis: POST /analysis/economic-impact with policy_id and dataset_id
14+
4. Check status: GET /analysis/economic-impact/{report_id} - repeat until status="completed"
15+
5. Review results: The completed response includes decile_impacts and program_statistics
916
"""
1017

1118
import math

src/policyengine_api/api/datasets.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ def list_datasets(session: Session = Depends(get_session)):
2323
2424
Returns datasets that can be used with the /analysis/economic-impact endpoint.
2525
Each dataset represents population microdata for a specific country and year.
26+
27+
USAGE: For UK analysis, look for datasets with names containing "uk" or "frs".
28+
For US analysis, look for datasets with names containing "us" or "cps".
29+
Use the dataset's id when calling /analysis/economic-impact.
2630
"""
2731
datasets = session.exec(select(Dataset)).all()
2832
return datasets

src/policyengine_api/api/parameters.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,31 @@
2121
@router.get("/", response_model=List[ParameterRead])
2222
@cache(expire=3600) # Cache for 1 hour
2323
def list_parameters(
24-
skip: int = 0, limit: int = 100, session: Session = Depends(get_session)
24+
skip: int = 0,
25+
limit: int = 100,
26+
search: str | None = None,
27+
session: Session = Depends(get_session),
2528
):
26-
"""List available parameters with pagination.
29+
"""List available parameters with pagination and search.
2730
2831
Parameters are policy levers (e.g. tax rates, thresholds, benefit amounts)
2932
that can be modified in reforms. Use parameter names when creating policies.
33+
34+
Use the `search` parameter to filter by parameter name, label, or description.
35+
For example: search="basic_rate" or search="income tax"
3036
"""
37+
query = select(Parameter)
38+
39+
if search:
40+
search_filter = (
41+
Parameter.name.contains(search)
42+
| Parameter.label.contains(search)
43+
| Parameter.description.contains(search)
44+
)
45+
query = query.where(search_filter)
46+
3147
parameters = session.exec(
32-
select(Parameter).order_by(Parameter.name).offset(skip).limit(limit)
48+
query.order_by(Parameter.name).offset(skip).limit(limit)
3349
).all()
3450
return parameters
3551

src/policyengine_api/api/policies.py

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,109 @@
33
Policies represent tax-benefit parameter reforms that can be compared against
44
baseline (current law). Create a policy, then use its ID with the household
55
calculation or economic impact endpoints to see the reform's effects.
6+
7+
WORKFLOW: To analyze a policy reform (e.g. lowering UK basic income tax rate to 16%):
8+
9+
1. Search for the parameter: GET /parameters?search=basic_rate
10+
2. Note the parameter_id from the results
11+
3. Create a policy with parameter values:
12+
POST /policies
13+
{
14+
"name": "Lower basic rate to 16p",
15+
"description": "Reduce UK basic income tax rate from 20p to 16p",
16+
"parameter_values": [
17+
{
18+
"parameter_id": "<uuid-from-step-1>",
19+
"value_json": 0.16,
20+
"start_date": "2026-01-01T00:00:00Z",
21+
"end_date": null
22+
}
23+
]
24+
}
25+
4. Test on a household: POST /household/impact with the policy_id
26+
5. Run population analysis: POST /analysis/economic-impact with policy_id and dataset_id
27+
6. Poll GET /analysis/economic-impact/{report_id} until status="completed"
628
"""
729

30+
from datetime import datetime
831
from typing import List
932
from uuid import UUID
1033

1134
from fastapi import APIRouter, Depends, HTTPException
1235
from sqlmodel import Session, select
1336

14-
from policyengine_api.models import Policy, PolicyCreate, PolicyRead
37+
from policyengine_api.models import (
38+
Parameter,
39+
ParameterValue,
40+
Policy,
41+
PolicyCreate,
42+
PolicyRead,
43+
)
1544
from policyengine_api.services.database import get_session
1645

1746
router = APIRouter(prefix="/policies", tags=["policies"])
1847

1948

2049
@router.post("/", response_model=PolicyRead)
2150
def create_policy(policy: PolicyCreate, session: Session = Depends(get_session)):
22-
"""Create a new policy reform.
51+
"""Create a new policy reform with parameter values.
2352
2453
Policies define changes to tax-benefit parameters. After creating a policy,
2554
use its ID with /household/calculate or /analysis/economic-impact to see effects.
55+
56+
Include parameter_values in the request to specify which parameters to change:
57+
{
58+
"name": "Lower basic rate to 16p",
59+
"description": "Reduce UK basic income tax rate from 20p to 16p",
60+
"parameter_values": [
61+
{
62+
"parameter_id": "uuid-from-parameters-search",
63+
"value_json": 0.16,
64+
"start_date": "2026-01-01T00:00:00Z",
65+
"end_date": null
66+
}
67+
]
68+
}
2669
"""
27-
db_policy = Policy.model_validate(policy)
70+
# Create the policy
71+
db_policy = Policy(name=policy.name, description=policy.description)
2872
session.add(db_policy)
73+
session.flush() # Get the policy ID before adding parameter values
74+
75+
# Create associated parameter values
76+
for pv_data in policy.parameter_values:
77+
# Validate parameter exists
78+
param = session.get(Parameter, pv_data["parameter_id"])
79+
if not param:
80+
raise HTTPException(
81+
status_code=404,
82+
detail=f"Parameter {pv_data['parameter_id']} not found",
83+
)
84+
85+
# Parse dates
86+
start_date = (
87+
datetime.fromisoformat(pv_data["start_date"].replace("Z", "+00:00"))
88+
if isinstance(pv_data["start_date"], str)
89+
else pv_data["start_date"]
90+
)
91+
end_date = None
92+
if pv_data.get("end_date"):
93+
end_date = (
94+
datetime.fromisoformat(pv_data["end_date"].replace("Z", "+00:00"))
95+
if isinstance(pv_data["end_date"], str)
96+
else pv_data["end_date"]
97+
)
98+
99+
# Create parameter value
100+
db_pv = ParameterValue(
101+
parameter_id=pv_data["parameter_id"],
102+
value_json=pv_data["value_json"],
103+
start_date=start_date,
104+
end_date=end_date,
105+
policy_id=db_policy.id,
106+
)
107+
session.add(db_pv)
108+
29109
session.commit()
30110
session.refresh(db_policy)
31111
return db_policy

src/policyengine_api/models/policy.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,29 @@ class Policy(PolicyBase, table=True):
2929

3030

3131
class PolicyCreate(PolicyBase):
32-
"""Schema for creating policies."""
33-
34-
pass
32+
"""Schema for creating policies.
33+
34+
When creating a policy with parameter values, provide a list of
35+
parameter value definitions. Each parameter value needs:
36+
- parameter_id: UUID of the parameter to modify
37+
- value_json: The new value (number, string, or nested object)
38+
- start_date: When this value takes effect
39+
- end_date: Optional end date (null for indefinite)
40+
41+
Example:
42+
{
43+
"name": "Lower basic rate to 16p",
44+
"description": "Reduce UK basic income tax rate",
45+
"parameter_values": [{
46+
"parameter_id": "uuid-here",
47+
"value_json": 0.16,
48+
"start_date": "2026-01-01T00:00:00Z",
49+
"end_date": null
50+
}]
51+
}
52+
"""
53+
54+
parameter_values: list[dict] = []
3555

3656

3757
class PolicyRead(PolicyBase):

0 commit comments

Comments
 (0)