Skip to content

feat: add run_reports tool with filter parser - #143

Open
humedi wants to merge 15 commits into
alyf-de:developfrom
humedi:run-reports-with-filter-parser
Open

humedi wants to merge 15 commits into
alyf-de:developfrom
humedi:run-reports-with-filter-parser

Conversation

@humedi

@humedi humedi commented Sep 21, 2026 •

Copy link
Copy Markdown
Collaborator

Adds a read-only run_report agent tool so Ask ALYF can execute Report queries with the same permission and filter rules as Desk.

  • get_report_filters (tool) returns the declared filters for a specific report. The agent is prompted to call this before run_report.
  • run_report takes a report name and optional filters. Query Report, Script Report, and Custom Report go through frappe.desk.query_report.run, with ignore_prepared_report always on. With no filters, are_default_filters stays true, so a custom report keeps its saved filters.
  • In run_report the Report Report Builder uses Report.get_data instead. Unknown fieldnames are rejected. MultiSelectList values become ["in", values]. A Link value is rejected unless the user can read or select that document. Results stop at 200 rows. truncated is set when more rows existed.
  • Filter definitions are read in extract_report_filters, including objects pulled in with $.extend or Object.assign. Date defaults are resolved to concrete values. A Company link without a default uses the user default. A Fiscal Year default is filled (only when ERPNext is installed).
  • A failed run is retried once without optional Check filters that match their default. The retry lists those names in omitted_filters. If it still fails, the tool returns error, report_name, applied_filters, and allowed_filters, so the agent can handle the failed request.
  • The Ask ALYF Skill erpnext-report-filters is a first pass. It is not fully checked and is meant to be adjusted in use. It is installed on install and migrate when ERPNext is present and the skill does not exist yet, for the Ask ALYF User role only. An existing skill is left unchanged. When the skill is available, the agent reads it before running an ERPNext report.
  • Tool-call labels are "Looking up report filters" and "Running a report".

Frappe core: query_report.run takes optional js_filters and passes them to validate_filters_permissions. Ask ALYF uses that only for Query Report, Script Report, and Custom Report. Report Builder does not call run. It checks Link permissions through validate_filters_permissions on its own.

resolves #135

- add `get_report_filters` tool to retrieve filter options for reports, parses js under the hood for filters.
- add `run_report` tool for report generation.
- Removed the `get_report_filters` method from the toolset as it is no longer needed.
- Updated the `run_report` function to directly retrieve filters and handle exceptions more gracefully.
- Adjusted the agent's tool call logic to reflect the removal of the `get_report_filters` call.
- Reformatted the `JS_FILTER_KEYS` for improved readability.
- Removed an unnecessary blank line in the `run_report` method of the toolset.
- Updated `get_report_filters` to include JavaScript filters alongside document filters.
- Modified `run_report` to handle filters more robustly, ensuring compatibility with the new structure.
- Added default values for "Company" and "Fiscal Year" link fields based on user settings and current date.
@greptile-apps

greptile-apps Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium impact] The latest changes appear safe to merge; moving the skill preserves installation and access.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Agent requests a report] --> B[get_report_filters]
  B --> C[run_report]
  C --> D{Report Builder?}
  D -->|Yes| E[Check filters and Link permissions]
  E --> F[Report.get_data]
  D -->|No| G[query_report.run]
  F --> H[Return at most 200 rows]
  G --> H
  C -->|Run fails| I[Retry without optional default Check filters]
  I --> J[Return result or error]
Loading

Reviews (9) · Last reviewed commit: "refactor: remove fixtures and put report..." · Reviewed by Greptile

Comment thread ask_alyf/ask_alyf/tools.py Outdated
Comment thread ask_alyf/ask_alyf/tools.py Outdated
…in run_report

- Updated `_find_js_for_ident` to improve efficiency by only using the first two parts of the identifier, its enough for ERPNext.
- Enhanced `_read_quoted` to handle string decoding more robustly.
- Improved error handling in `run_report` to provide clearer context in the returned error message.
Comment thread ask_alyf/ask_alyf/tools.py Outdated
Comment thread ask_alyf/ask_alyf/tools.py Outdated
- Removed the unnecessary _has_own_fieldname function to streamline the filter extraction process.
- Directly appended valid rows from JavaScript objects to the results list.
@humedi
humedi requested a review from barredterra September 24, 2026 11:02
Comment thread ask_alyf/ask_alyf/tools.py
humedi added 2 commits October 1, 2026 20:56
- Move report JS filter parsing into extract_report_filters.py, with $.extend and date defaults.
- On Report Builder, keep only those fieldnames and check Link permissions.
- Cap every report at 200 rows.
- If a run fails, retry without optional checks that match their default.
- Install the ERPNext report skill so the agent passes the checks those reports expect.
- Cover Report Builder filter rejection and the skill install with tests.
@humedi
humedi requested a review from barredterra October 1, 2026 19:20
Comment thread ask_alyf/hooks.py
Comment on lines +11 to +12
after_install = ["ask_alyf.ask_alyf.install.ensure_erpnext_report_skill"]
after_migrate = ["ask_alyf.ask_alyf.install.ensure_erpnext_report_skill"]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of building the fixture sync manually, could we just reuse this frappe standard hook? https://docs.frappe.io/framework/user/en/python-api/hooks#fixtures

This comment was marked as outdated.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intention was to not override the Skill, if it once exists

cdbdd99

@github-actions

Copy link
Copy Markdown

Here is a summary of the .po file changes:

  • Changed files: 9
  • Added files: 0
  • Removed files: 0
  • Files in similar change-size groups within 2% tolerance: 9
  • Added or changed translations detected: 9 across 9 file(s)

Similar Change-Size Groups

  • Around +5 / -1 lines: 9 files (ar.po, bn.po, de.po, es.po, hi.po, ja.po, pt.po, ru.po, zh_CN.po)
ar (ask_alyf/locale/ar.po) — 1 entries

ar (ask_alyf/locale/ar.po)

Status Msgid Previous Current
added Looking up report filters empty البحث عن عوامل تصفية التقرير
bn (ask_alyf/locale/bn.po) — 1 entries

bn (ask_alyf/locale/bn.po)

Status Msgid Previous Current
added Looking up report filters empty রিপোর্ট ফিল্টার খোঁজা হচ্ছে
de (ask_alyf/locale/de.po) — 1 entries

de (ask_alyf/locale/de.po)

Status Msgid Previous Current
added Looking up report filters empty Berichtsfilter werden gesucht
es (ask_alyf/locale/es.po) — 1 entries

es (ask_alyf/locale/es.po)

Status Msgid Previous Current
added Looking up report filters empty Buscando los filtros del informe
hi (ask_alyf/locale/hi.po) — 1 entries

hi (ask_alyf/locale/hi.po)

Status Msgid Previous Current
added Looking up report filters empty रिपोर्ट फ़िल्टर खोजे जा रहे हैं
ja (ask_alyf/locale/ja.po) — 1 entries

ja (ask_alyf/locale/ja.po)

Status Msgid Previous Current
added Looking up report filters empty レポートのフィルターを検索しています
pt (ask_alyf/locale/pt.po) — 1 entries

pt (ask_alyf/locale/pt.po)

Status Msgid Previous Current
added Looking up report filters empty A procurar filtros do relatório
ru (ask_alyf/locale/ru.po) — 1 entries

ru (ask_alyf/locale/ru.po)

Status Msgid Previous Current
added Looking up report filters empty Поиск фильтров отчета
zh_Hans_CN (ask_alyf/locale/zh_CN.po) — 1 entries

zh_Hans_CN (ask_alyf/locale/zh_CN.po)

Status Msgid Previous Current
added Looking up report filters empty 正在查找报表筛选条件

@github-actions

Copy link
Copy Markdown
ask_alyf/locale/main.pot — 1 added, 0 removed, 0 corrected

ask_alyf/locale/main.pot

Status Previous Current
added empty Looking up report filters

@barredterra barredterra left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review notes below. The first three (all in run_report error handling) look like the ones to fix before merge. Dropping the retry and letting errors go through _on_tool_error would solve all three.

row
and row.get("fieldtype") == "Check"
and not row.get("reqd")
and cint(row.get("default")) == cint(value)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The retry drops optional Check filters whose value equals the default, also when the default is 1. In query_report.run, a missing Check is falsy, not "use the default". So the retry changes the meaning of the report.

Example: General Ledger with include_default_book_entries=1 (default 1) fails for another reason, e.g. a bad date. The retry runs without the filter, so default book entries are excluded. The agent gets different totals plus an omitted_filters note, and the original error is lost.

try:
meta = get_report_filters(report_name)
return _execute_report(report_name, meta, applied_filters, are_default_filters=not filters)
except Exception as error:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This catches every exception and returns str(error) to the model. That gets around _on_tool_error in agent.py, which on purpose shows only the exception type for errors that are not Validation or Permission errors. SQL errors, KeyErrors etc. from script reports would then put query text, table/column names or internal values into the LLM context, the conversation and the traces.

result = _execute_report(report_name, meta, narrowed, are_default_filters=False)
result["omitted_filters"] = omitted
return result
except Exception:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The retry runs after any exception: timeouts, lock waits, permission or DB errors. It reruns the full report without a rollback and swallows the second error. On a heavy report this doubles the load. On Postgres, the aborted transaction makes the retry fail too, and later DB calls in the same agent job can also fail.

if token.type not in ("linecomment", "multilinecomment")
]
rows = []
for index, token in enumerate(tokens):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every {...} in the report JS that has a depth-1 fieldname: key counts as a report filter. That includes frappe.prompt / dialog fields, get_query return values and column definitions in onload or button handlers. Such fields then show up as allowed filters, and for Report Builder _builder_filters turns them into WHERE conditions. Maybe limit parsing to the filters: [...] array of the frappe.query_reports[...] object?

name = row["fieldname"]
if name in seen:
continue
seen.add(name)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extended base scripts are read before the report's own script, and the first fieldname seen wins. When a report extends a base set of filters (e.g. erpnext.financial_statements) and then redefines one with a different default or reqd, the base version is returned. Reading the report's own script first (or letting later definitions win) would fix this.

break
elif (
depth == 1
and tokens[index].type == "name"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only name tokens are matched as keys. Quoted keys such as "fieldname": "company" are string tokens, so filters written in JSON style (common in custom report javascript) are not found. The report then shows no filters.

"""Install the ERPNext report skill once. Skip when ERPNext is absent or the skill was edited."""
if "erpnext" not in frappe.get_installed_apps():
return
if frappe.db.exists("Ask ALYF Skill", REPORT_SKILL_NAME):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring says "skip when ... the skill was edited", but this skips whenever the skill exists, edited or not. Two effects: existing sites never get an improved REPORT_SKILL_DESCRIPTION, and a skill that an admin deleted on purpose comes back on every bench migrate. At least the docstring should match the behaviour.

from frappe.desk.query_report import get_report_doc, get_script

report = get_report_doc(report_name) # checks permissions
script = get_script(report_name)["script"] or ""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each run_report call runs get_report_filters again: get_report_doc twice (directly and inside get_script), JS render plus tokenizing (also of extended files), maybe fiscal year queries. Then run() loads the Report again. A normal "get filters, then run" turn parses the JS twice and loads the Report about 5 times. Not urgent, but only js_filters is needed for the run.

}


def _cap_report_rows(payload):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping only the first rows cuts off the totals row that many query reports (and add_totals_row) add at the end, e.g. Trial Balance or General Ledger. The agent may then add up the partial rows and report a wrong total. Maybe keep the last row when it is a total, or tell the agent that totals were cut off.

@@ -0,0 +1,362 @@
"""Extract `filters` from `frappe.query_reports["X"] = {...}` JS files."""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a new 362-line JS tokenizer and expression evaluator (dates, fiscal years, $.extend lookup) with no tests. The new tests cover only the Report Builder path, which does not use this parser. A few tests that parse real ERPNext report JS files (e.g. General Ledger, a financial statement) would catch regressions when ERPNext changes its JS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add tool for running query reports

2 participants