Aker Deploy commited on
Commit
f9d065c
Β·
1 Parent(s): 57ffeae

fix(agent+chart): no invented date filters, no empty chart bars (mirror of main)

Browse files
Files changed (2) hide show
  1. app/graph/components.py +16 -3
  2. app/graph/nodes.py +28 -2
app/graph/components.py CHANGED
@@ -139,19 +139,32 @@ def from_expiring_leases(r: dict[str, Any]) -> list[dict]:
139
 
140
 
141
  def from_compare_units(r: dict[str, Any]) -> list[dict]:
142
- """Grouped bar chart for intra-property unit-vs-unit comparison."""
 
 
 
 
 
 
143
  series = r.get("series") or []
144
  rows = r.get("rows") or []
145
  unit_numbers = r.get("unit_numbers") or [row["unit_number"] for row in rows]
146
  if not series or not unit_numbers:
147
  return []
148
- # Recharts expects an array of objects keyed by category.
149
  chart_rows = []
150
  for s in series:
 
 
 
 
 
151
  row = {"dimension": s["dimension"]}
152
  for u in unit_numbers:
153
- row[u] = s["values"].get(u)
154
  chart_rows.append(row)
 
 
155
  return [{
156
  "type": "comparison_chart",
157
  "title": "Unit Comparison",
 
139
 
140
 
141
  def from_compare_units(r: dict[str, Any]) -> list[dict]:
142
+ """Grouped bar chart for intra-property unit-vs-unit comparison.
143
+
144
+ Drops any dimension whose values are all-null across every unit so the
145
+ chart doesn't render an invisible axis (the rent-roll source can have
146
+ `monthly_rent` null on notice/move-out units, which previously surfaced
147
+ as a "rent" column with no bars and no explanation).
148
+ """
149
  series = r.get("series") or []
150
  rows = r.get("rows") or []
151
  unit_numbers = r.get("unit_numbers") or [row["unit_number"] for row in rows]
152
  if not series or not unit_numbers:
153
  return []
154
+
155
  chart_rows = []
156
  for s in series:
157
+ vals = s.get("values") or {}
158
+ # Skip dimensions where every unit returned null/empty β€” empty bars
159
+ # look like a bug to users even when the data simply isn't there.
160
+ if not any(v is not None for v in vals.values()):
161
+ continue
162
  row = {"dimension": s["dimension"]}
163
  for u in unit_numbers:
164
+ row[u] = vals.get(u)
165
  chart_rows.append(row)
166
+ if not chart_rows:
167
+ return []
168
  return [{
169
  "type": "comparison_chart",
170
  "title": "Unit Comparison",
app/graph/nodes.py CHANGED
@@ -279,7 +279,20 @@ def _build_tools(scope: ScopeDecision):
279
 
280
  @tool
281
  def get_move_outs(since: str | None = None, until: str | None = None, property_code: str | None = None) -> dict:
282
- """Leases with a Move Out date set, optionally filtered by date range (YYYY-MM-DD). Use for 'who is moving out this quarter?' or 'show move-outs since 2026-01-01'. In compare mode pass `property_code`."""
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  c = _pick_code(property_code)
284
  if isinstance(c, dict): return c
285
  return SQL_TOOLS["get_move_outs"]["fn"](c, since=since, until=until)
@@ -295,7 +308,20 @@ def _build_tools(scope: ScopeDecision):
295
 
296
  @tool
297
  def compare_units(unit_numbers: list[str], dimensions: list[str] | None = None, property_code: str | None = None) -> dict:
298
- """Side-by-side comparison of 2+ units WITHIN ONE property. dimensions can include rent, sqft, market_rent, balance, bedrooms, bathrooms. In compare mode pass `property_code` to pick which property."""
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  c = _pick_code(property_code)
300
  if isinstance(c, dict): return c
301
  return SQL_TOOLS["compare_units"]["fn"](
 
279
 
280
  @tool
281
  def get_move_outs(since: str | None = None, until: str | None = None, property_code: str | None = None) -> dict:
282
+ """Leases with a Move Out date set, optionally filtered by date range.
283
+
284
+ DO NOT invent date filters for vague language. Pass `since`/`until` ONLY if the
285
+ user wrote an EXPLICIT calendar reference (a year, a month-name, a quarter, or
286
+ a literal date like '2026-01-01').
287
+
288
+ - "who is moving out soon?" β†’ call with NO args (returns all)
289
+ - "list 3 units moving out soon" β†’ call with NO args (returns all, agent picks 3)
290
+ - "any tenants moving out?" β†’ call with NO args
291
+ - "moving out this quarter" β†’ infer since/until of the CURRENT quarter
292
+ - "moving out after January 2026" β†’ since='2026-01-01'
293
+ - "moving out between Jan and Mar 2026" β†’ since='2026-01-01', until='2026-03-31'
294
+
295
+ Both args are YYYY-MM-DD strings. In compare mode pass `property_code`."""
296
  c = _pick_code(property_code)
297
  if isinstance(c, dict): return c
298
  return SQL_TOOLS["get_move_outs"]["fn"](c, since=since, until=until)
 
308
 
309
  @tool
310
  def compare_units(unit_numbers: list[str], dimensions: list[str] | None = None, property_code: str | None = None) -> dict:
311
+ """Side-by-side comparison of 2+ units WITHIN ONE property.
312
+
313
+ Dimensions: rent | monthly_rent | market_rent | sqft | balance | bedrooms | bathrooms.
314
+
315
+ Numbers reflect the LATEST snapshot only (the `leases` / `units` tables).
316
+ ALWAYS state that in your reply (e.g. "as of the latest snapshot").
317
+
318
+ NULL handling: if a returned value is null (e.g. `monthly_rent` is often null
319
+ for units on notice / vacant / pending move-out) you MUST call that out
320
+ explicitly in your reply β€” say "monthly rent is not recorded for unit X
321
+ (notice status)" rather than leaving it as a silent gap. The chart drops
322
+ all-null dimensions automatically so users don't see empty bars.
323
+
324
+ In compare mode pass `property_code` to pick which property."""
325
  c = _pick_code(property_code)
326
  if isinstance(c, dict): return c
327
  return SQL_TOOLS["compare_units"]["fn"](