Skip to content

Commit

Permalink
Split explanations and add used data (#19)
Browse files Browse the repository at this point in the history
* show tree
split explanations

* Working tree

* add title
  • Loading branch information
anneschuth authored Feb 16, 2025
1 parent b0447b2 commit 02dc944
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 58 deletions.
99 changes: 89 additions & 10 deletions machine/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down Expand Up @@ -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:
Expand Down
57 changes: 45 additions & 12 deletions web/routers/laws.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
},
)
2 changes: 1 addition & 1 deletion web/templates/partials/tiles/base_tile.html
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ <h3 class="text-lg font-semibold text-gray-900 mb-2">
<span class="text-sm font-medium text-gray-500">U komt niet in aanmerking</span>
<button @click="showExplanation = true"
class="ml-3 text-sm text-gray-400 hover:text-gray-900 underline"
hx-get="/laws/explain-path?service={{ service }}&law={{ law|urlencode }}&bsn={{ bsn }}"
hx-get="/laws/explain-panel?service={{ service }}&law={{ law|urlencode }}&bsn={{ bsn }}"
hx-target="#explanation-panel-{{ law|replace('/', '-') }}"
hx-swap="innerHTML">
waarom?
Expand Down
8 changes: 8 additions & 0 deletions web/templates/partials/tiles/components/explanation.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<div class="space-y-2">
<h4 class="font-medium text-gray-900">Uitleg berekening</h4>
{% if error %}
<p class="text-red-600">{{ error }}</p>
{% else %}
<p class="text-sm text-gray-600">{{ explanation }}</p>
{% endif %}
</div>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{% from "macros/logos.html" import org_logo %}

<div class="fixed inset-y-0 right-0 w-1/3 bg-white shadow-xl transform transition-transform duration-300 ease-in-out z-50 overflow-y-auto"
<div class="fixed inset-y-0 right-0 w-1/2 bg-white shadow-xl transform transition-transform duration-300 ease-in-out z-50 overflow-y-auto"
x-data="{ show: true, showTechnical: false }"
x-show="show"
x-init="$nextTick(() => { show = true })"
Expand Down Expand Up @@ -63,13 +63,82 @@ <h4 class="font-medium text-gray-900">Uitkomst</h4>
</div>

<!-- Burger-friendly explanation -->
<div class="space-y-2">
<h4 class="font-medium text-gray-900">Uitleg berekening</h4>
{% if error %}
<p class="text-red-600">{{ error }}</p>
<div
hx-get="/laws/explanation?service={{ service }}&law={{ law|urlencode }}&bsn={{ bsn }}"
hx-trigger="load"
hx-swap="outerHTML"
class="h-full bg-white rounded-lg shadow"
>
{% include "partials/loading.html" %}
</div>

<h4 class="font-medium text-gray-900">Gebruikte gegevens</h4>
<div class="max-w-7xl mx-auto">
{% macro render_value(value) %}
{% if value is none %}
<span class="text-gray-400">-</span>
{% 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) %}
<div x-data="{ open: false }" class="text-sm leading-5">
<div class="hover:bg-gray-50 transition-colors duration-150 py-0.5 flex items-start cursor-pointer"
@click="open = !open">
<div class="text-sm leading-5 py-0.5">
<span class="text-gray-600">{{ key|title|replace('_', ' ') }}: </span>
<span class="ml-2 text-blue-600"> {{ render_value(node.result) }}</span>
</div>
<div class="min-w-0 flex items-center">
<svg :class="{'rotate-90': open}"
class="transform transition-transform duration-200 w-4 h-4 text-gray-500 mr-1"
viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M7.293 4.707a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L10.586 10 7.293 6.707a1 1 0 010-1.414z"
clip-rule="evenodd"/>
</svg>
<span class="text-green-600">
{% if node.service %}
Resultaat van {{ node.service|replace('_', ' ')|title }}
{% if node.law %}({{ node.law|replace('_', ' ')|title }}){% endif %}
{% else %}
{{ key|title|replace('_', ' ') }}
{% endif %}
</span>
</div>
</div>

<div x-show="open" class="ml-6 border-l border-gray-200 pl-4">
{% if node.children %}
{% for child_key, child_node in node.children.items() %}
{{ render_node(child_key, child_node) }}
{% endfor %}
{% endif %}
</div>
</div>
{% else %}
<p class="text-sm text-gray-600">{{ explanation }}</p>
<div class="text-sm leading-5 py-0.5">
<span class="text-gray-600">{{ key|title|replace('_', ' ') }}: </span>
<span class="ml-2 text-blue-600">{{ render_value(node.result) }}</span>
</div>
{% endif %}
{% endmacro %}

<div class="bg-gray-50 rounded-lg text-sm p-4 border border-gray-100">
{% for key, node in path.items() %}
{{ render_node(key, node) }}
{% endfor %}
</div>
</div>

<!-- Collapsible Technical Section -->
Expand Down Expand Up @@ -99,31 +168,6 @@ <h4 class="font-medium text-gray-900">Uitleg berekening</h4>
x-transition:leave-end="opacity-0 transform -translate-y-2"
class="mt-4 space-y-4">

<!-- Input Variables -->
<div class="space-y-2">
<h5 class="font-medium text-gray-900 text-sm">Gebruikte gegevens</h5>
<div class="space-y-2">
{% for key, value in input.items() %}
<div class="text-sm">
<span class="text-gray-600">{{ key }}: </span>
<span class="font-medium">
{% 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 %}
</span>
</div>
{% endfor %}
</div>
</div>

<!-- Technical Details -->
<div class="space-y-2">
<h5 class="font-medium text-gray-900 text-sm">Details</h5>
Expand All @@ -136,4 +180,5 @@ <h5 class="font-medium text-gray-900 text-sm">Details</h5>
</div>
</div>
</div>

</div>
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ <h4 class="text-sm font-medium text-gray-500 mb-1">Uw AOW pensioen is waarschijn
<button
@click="showExplanation = true"
class="ml-4 text-sm text-gray-400 hover:text-gray-900 border-b border-dotted border-gray-300 hover:border-gray-600"
hx-get="/laws/explain-path?service={{ service }}&law={{ law|urlencode }}&bsn={{ bsn }}"
hx-get="/laws/explain-panel?service={{ service }}&law={{ law|urlencode }}&bsn={{ bsn }}"
hx-target="#explanation-panel-{{ law|replace('/', '-') }}"
hx-swap="innerHTML">
waarom?
Expand Down
2 changes: 1 addition & 1 deletion web/templates/partials/tiles/law/kieswet/KIESRAAD.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ <h4 class="text-sm font-medium text-gray-500 mb-1">
<button
@click="showExplanation = true"
class="ml-4 text-sm text-gray-400 hover:text-gray-900 border-b border-dotted border-gray-300 hover:border-gray-600"
hx-get="/laws/explain-path?service={{ service }}&law={{ law|urlencode }}&bsn={{ bsn }}"
hx-get="/laws/explain-panel?service={{ service }}&law={{ law|urlencode }}&bsn={{ bsn }}"
hx-target="#explanation-panel-{{ law|replace('/', '-') }}"
hx-swap="innerHTML">
waarom?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ <h4 class="text-sm font-medium text-gray-500 mb-1">Uw bijstandsuitkering is waar
<button
@click="showExplanation = true"
class="ml-4 text-sm text-gray-400 hover:text-gray-900 border-b border-dotted border-gray-300 hover:border-gray-600"
hx-get="/laws/explain-path?service={{ service }}&law={{ law|urlencode }}&bsn={{ bsn }}"
hx-get="/laws/explain-panel?service={{ service }}&law={{ law|urlencode }}&bsn={{ bsn }}"
hx-target="#explanation-panel-{{ law|replace('/', '-') }}"
hx-swap="innerHTML">
waarom?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ <h4 class="text-sm font-medium text-gray-600 mb-2">Uw zorgtoeslag is waarschijnl
<button
@click="showExplanation = true"
class="ml-4 text-sm text-gray-400 hover:text-gray-900 border-b border-dotted border-gray-300 hover:border-gray-600"
hx-get="/laws/explain-path?service={{ service }}&law={{ law|urlencode }}&bsn={{ bsn }}"
hx-get="/laws/explain-panel?service={{ service }}&law={{ law|urlencode }}&bsn={{ bsn }}"
hx-target="#explanation-panel-{{ law|replace('/', '-') }}"
hx-swap="innerHTML">
waarom?
Expand Down

0 comments on commit 02dc944

Please sign in to comment.