-
Notifications
You must be signed in to change notification settings - Fork 15
Add auto_assign_monthly_targets tool #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| from datetime import date | ||
|
|
||
| from src.server import _shared | ||
| from src.server._shared import dollars_to_milliunits, serialize, serialize_list | ||
|
|
||
|
|
@@ -7,6 +9,12 @@ | |
| "Pass [] to return all fields. Pass a custom list to override the default." | ||
| ) | ||
|
|
||
| AUTO_ASSIGN_SKIP_GROUPS = { | ||
| "Internal Master Category", | ||
| "Credit Card Payments", | ||
| "Hidden Categories", | ||
| } | ||
|
|
||
|
|
||
| @_shared.mcp.tool() | ||
| @_shared.handle_errors | ||
|
|
@@ -214,3 +222,52 @@ async def update_category_for_month( | |
| month, category_id, dollars_to_milliunits(budgeted), plan_id | ||
| ) | ||
| return serialize(cat, exclude_fields=exclude_fields) | ||
|
|
||
|
|
||
| @_shared.mcp.tool() | ||
| @_shared.handle_errors | ||
| async def auto_assign_monthly_targets(plan_id: str, month: str = "current") -> str: | ||
| """Assign budgets to all categories based on their goal targets. | ||
|
|
||
| Mirrors YNAB's Auto-Assign -> Monthly Targets button. For every category | ||
| that has a goal_target set, assigns the goal amount as the budgeted value | ||
| for the given month. Skips hidden, deleted, and internal groups | ||
| (Internal Master Category, Credit Card Payments, Hidden Categories). | ||
|
|
||
| Args: | ||
| plan_id: The plan ID (use list_plans to find available IDs) | ||
| month: Month in YYYY-MM-DD format (e.g. '2026-05-01') or 'current'. Defaults to current month. | ||
| """ | ||
| import json | ||
|
|
||
| if month == "current": | ||
| today = date.today() | ||
| month = today.replace(day=1).strftime("%Y-%m-%d") | ||
|
|
||
| groups = await _shared.cache.get_categories(plan_id) | ||
| assignments = [] | ||
| for group in groups: | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Design thought, not a blocker: if
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. Wrapped each update_category_for_month call in try/except and added a status field per assignment ("ok" or "error"). categories_assigned now counts only successes, and the agent can see exactly which categories failed and why. |
||
| if group.name in AUTO_ASSIGN_SKIP_GROUPS or group.hidden or group.deleted: | ||
| continue | ||
| for cat in group.categories: | ||
| if cat.hidden or cat.deleted or not cat.goal_target: | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Big-picture question before we land this: YNAB's actual "Auto-Assign Monthly Targets" button only fires on monthly-cadence goals ( Could we add a
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch. The broader scope was not intentional. Added the goal_type/cadence filter to match YNAB's UI: goal_type == "MF" or (goal_type == "NEED" and goal_cadence == 1). TB and other non-monthly goals are now skipped. |
||
| continue | ||
| updated = await _shared.cache.update_category_for_month( | ||
| month, cat.id, cat.goal_target, plan_id | ||
| ) | ||
| assignments.append({ | ||
| "name": updated.name, | ||
| "group": group.name, | ||
| "budgeted": updated.budgeted / 1000, | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Heads up, PR #15 (which I'm hoping to land soon) introduces a
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Already done since #15 has landed. Synced upstream and switched to milliunits_to_dollars() in this commit. |
||
| }) | ||
|
|
||
| total = sum(a["budgeted"] for a in assignments) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two thoughts here: summing dollar floats then rounding can drift on bigger plans, and once #15 lands we'll have the total_mu += updated.budgeted
...
"total_budgeted": milliunits_to_dollars(total_mu),Same fix as what I just did to
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Switched to accumulating in milliunits and converting once at the end via milliunits_to_dollars() (which landed with #15, already synced). |
||
| return json.dumps( | ||
| { | ||
| "month": month, | ||
| "categories_assigned": len(assignments), | ||
| "total_budgeted": round(total, 2), | ||
| "assignments": assignments, | ||
| }, | ||
| indent=2, | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tiny nit: can we hoist this up to the module-level imports next to
from datetime import date? Function-local imports always make me do a double-take.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hoisted.