diff --git a/machine/context.py b/machine/context.py index 1b1c639..76247dd 100644 --- a/machine/context.py +++ b/machine/context.py @@ -71,6 +71,61 @@ class PathNode: children: list["PathNode"] = field(default_factory=list) +def flatten_path_nodes(root): + def is_path_node(obj): + return isinstance(obj, PathNode) + + # Iterative flattening approach + flattened = {} + stack = [(root, None)] + + while stack: + node, service_parent = stack.pop() + + if not is_path_node(node): + continue + + path = node.details.get("path") + if isinstance(path, str) and path.startswith("$"): + path = path[1:] + + # Handle resolve nodes + if node.type == "resolve" and path and isinstance(path, str): + resolve_entry = { + "result": node.result, + } + + if service_parent and path not in service_parent.setdefault("children", {}): + service_parent.setdefault("children", {})[path] = resolve_entry + elif path not in flattened: + flattened[path] = resolve_entry + + # Handle service_evaluation nodes + elif node.type == "service_evaluation" and path and isinstance(path, str): + service_entry = { + "result": node.result, + "service": node.details.get("service"), + "law": node.details.get("law"), + "children": {}, + } + + if service_parent: + service_parent.setdefault("children", {})[path] = service_entry + else: + flattened[path] = service_entry + + # Prepare to process children with this service_evaluation as parent + for child in reversed(node.children): + stack.append((child, service_entry)) + continue + + # Add children to the stack for further processing + for child in reversed(node.children): + stack.append((child, service_parent)) + + return flattened + + @dataclass class RuleContext: """Context for rule evaluation""" @@ -269,18 +324,42 @@ async def _resolve_from_service(self, path, service_ref, spec): logger.debug(f"Resolving from {service_ref['service']} field {service_ref['field']} ({parameters})") - result = await self.service_provider.evaluate( - service_ref["service"], - service_ref["law"], - parameters, - reference_date, - self.overwrite_input, - requested_output=service_ref["field"], + # Create service evaluation node + service_node = PathNode( + type="service_evaluation", + name=f"Service call: {service_ref['service']}.{service_ref['law']}", + result=None, + details={ + "service": service_ref["service"], + "law": service_ref["law"], + "field": service_ref["field"], + "reference_date": reference_date, + "parameters": parameters, + "path": path, + }, ) + self.add_to_path(service_node) - value = result.output.get(service_ref["field"]) - self.values_cache[cache_key] = value - return value + try: + result = await self.service_provider.evaluate( + service_ref["service"], + service_ref["law"], + parameters, + reference_date, + self.overwrite_input, + requested_output=service_ref["field"], + ) + + value = result.output.get(service_ref["field"]) + self.values_cache[cache_key] = value + + # Update the service node with the result and add child path + service_node.result = value + service_node.children.append(result.path) + + return value + finally: + self.pop_path() async def _resolve_from_source(self, source_ref, table, df): if "select_on" in source_ref: diff --git a/web/routers/laws.py b/web/routers/laws.py index 9277989..7ec5bf3 100644 --- a/web/routers/laws.py +++ b/web/routers/laws.py @@ -6,6 +6,7 @@ from jinja2 import TemplateNotFound from explain.llm_service import llm_service +from machine.context import flatten_path_nodes from machine.service import Services from web.dependencies import TODAY, get_services, templates from web.services.profiles import get_profile_data @@ -190,8 +191,48 @@ def node_to_dict(node): } -@router.get("/explain-path") -async def explain_path( +@router.get("/explain-panel") +async def explain_panel( + request: Request, + service: str, + law: str, + bsn: str, + services: Services = Depends(get_services), +): + """Get an explanation panel""" + try: + law = unquote(law) + law, result, rule_spec = await evaluate_law(bsn, law, service, services) + flat_path = flatten_path_nodes(result.path) + return templates.TemplateResponse( + "partials/tiles/components/explanation_panel.html", + { + "request": request, + "service": service, + "law": law, + "rule_spec": rule_spec, + "input": result.input, + "result": result.output, + "requirements_met": result.requirements_met, + "path": flat_path, + "bsn": bsn, + }, + ) + except Exception as e: + print(f"Error in explain_path: {e}") + return templates.TemplateResponse( + "partials/tiles/components/explanation_panel.html", + { + "request": request, + "error": "Er is een fout opgetreden bij het genereren van de uitleg. Probeer het later opnieuw.", + "service": service, + "law": law, + }, + ) + + +@router.get("/explanation") +async def explanation( request: Request, service: str, law: str, @@ -226,26 +267,18 @@ async def explain_path( explanation = llm_service.generate_explanation(path_json, rule_spec_json) return templates.TemplateResponse( - "partials/tiles/components/path_explanation.html", + "partials/tiles/components/explanation.html", { "request": request, "explanation": explanation, - "service": service, - "law": law, - "rule_spec": rule_spec, - "input": result.input, - "result": result.output, - "requirements_met": result.requirements_met, }, ) except Exception as e: print(f"Error in explain_path: {e}") return templates.TemplateResponse( - "partials/tiles/components/path_explanation.html", + "partials/tiles/components/explanation.html", { "request": request, "error": "Er is een fout opgetreden bij het genereren van de uitleg. Probeer het later opnieuw.", - "service": service, - "law": law, }, ) diff --git a/web/templates/partials/tiles/base_tile.html b/web/templates/partials/tiles/base_tile.html index 6203b64..d3a00b5 100644 --- a/web/templates/partials/tiles/base_tile.html +++ b/web/templates/partials/tiles/base_tile.html @@ -183,7 +183,7 @@

U komt niet in aanmerking

-
-

Uitleg berekening

- {% if error %} -

{{ error }}

+
+ {% include "partials/loading.html" %} +
+ +

Gebruikte gegevens

+
+ {% macro render_value(value) %} + {% if value is none %} + - + {% elif value is boolean %} + {{ 'Ja' if value else 'Nee' }} + {% elif value is number %} + {% if value % 1 != 0 %} + {{ '%.5f'|format(value) }} + {% else %} + {{ value }} + {% endif %} + {% else %} + {{ value }} + {% endif %} + {% endmacro %} + + {% macro render_node(key, node) %} + {% if node is mapping and ('service' in node or 'children' in node) %} +
+
+
+ {{ key|title|replace('_', ' ') }}: + {{ render_value(node.result) }} +
+
+ + + + + {% if node.service %} + Resultaat van {{ node.service|replace('_', ' ')|title }} + {% if node.law %}({{ node.law|replace('_', ' ')|title }}){% endif %} + {% else %} + {{ key|title|replace('_', ' ') }} + {% endif %} + +
+
+ +
+ {% if node.children %} + {% for child_key, child_node in node.children.items() %} + {{ render_node(child_key, child_node) }} + {% endfor %} + {% endif %} +
+
{% else %} -

{{ explanation }}

+
+ {{ key|title|replace('_', ' ') }}: + {{ render_value(node.result) }} +
{% endif %} + {% endmacro %} + +
+ {% for key, node in path.items() %} + {{ render_node(key, node) }} + {% endfor %} +
@@ -99,31 +168,6 @@

Uitleg berekening

x-transition:leave-end="opacity-0 transform -translate-y-2" class="mt-4 space-y-4"> - -
-
Gebruikte gegevens
-
- {% for key, value in input.items() %} -
- {{ key }}: - - {% if value is boolean %} - {{ 'Ja' if value else 'Nee' }} - {% elif value is number %} - {% if 'euro' in key.lower() or 'income' in key.lower() or 'toeslag' in key.lower() %} - €{{ '%.2f'|format(value/100) }} - {% else %} - {{ value }} - {% endif %} - {% else %} - {{ value }} - {% endif %} - -
- {% endfor %} -
-
-
Details
@@ -136,4 +180,5 @@
Details
+ diff --git a/web/templates/partials/tiles/law/algemene_ouderdomswet/SVB.html b/web/templates/partials/tiles/law/algemene_ouderdomswet/SVB.html index b5725ab..c5be876 100644 --- a/web/templates/partials/tiles/law/algemene_ouderdomswet/SVB.html +++ b/web/templates/partials/tiles/law/algemene_ouderdomswet/SVB.html @@ -10,7 +10,7 @@

Uw AOW pensioen is waarschijn