+ }
+}'
+
+ EVALUATOR_ID="$(ax evaluators get "$EVALUATOR_NAME" --space "$ARIZE_SPACE_ID" --output json | read_json_id)"
+ echo "Created evaluator: $EVALUATOR_ID"
+fi
+
+task_json="$(ax tasks list --name "$TASK_NAME" --project "$PROJECT_NAME" --space "$ARIZE_SPACE_ID" --limit 100 --output json)"
+TASK_ID="$(printf '%s' "$task_json" | find_task_id)"
+if [[ -n "$TASK_ID" ]]; then
+ echo "Reusing continuous evaluation task: $TASK_ID"
+ exit 0
+fi
+
+EVALUATORS="$(jq -nc --arg evaluator_id "$EVALUATOR_ID" '[{
+ evaluator_id: $evaluator_id,
+ query_filter: "span_kind = '\''CHAIN'\''",
+ column_mappings: {
+ receipt_image: "attributes.input.value",
+ extraction: "attributes.output.value"
+ }
+}]')"
+
+ax tasks create-evaluation \
+ --name "$TASK_NAME" \
+ --task-type TEMPLATE_EVALUATION \
+ --project "$PROJECT_NAME" \
+ --space "$ARIZE_SPACE_ID" \
+ --evaluators "$EVALUATORS" \
+ --is-continuous \
+ --sampling-rate 1
+
+echo "Created continuous evaluation task for project: $PROJECT_NAME"
diff --git a/python/cookbooks/receipt_image_evals/tracing.py b/python/cookbooks/receipt_image_evals/tracing.py
new file mode 100644
index 0000000..12ce931
--- /dev/null
+++ b/python/cookbooks/receipt_image_evals/tracing.py
@@ -0,0 +1,27 @@
+"""Arize AX and OpenTelemetry setup for the receipt demo."""
+
+from __future__ import annotations
+
+import os
+
+from arize.otel import register
+from openinference.instrumentation import OITracer, TraceConfig
+from openinference.instrumentation.openai import OpenAIInstrumentor
+
+
+def initialize_tracing(instrumentation_scope: str) -> OITracer:
+ """Configure mandatory AX tracing before any OpenAI client is created."""
+ required = ("ARIZE_API_KEY", "ARIZE_SPACE_ID")
+ if not all(os.environ.get(name) for name in required):
+ raise RuntimeError("Tracing requires ARIZE_API_KEY and ARIZE_SPACE_ID before starting the receipt app.")
+ provider = register(
+ project_name=os.environ.get("ARIZE_PROJECT_NAME", "receipt-image-evals"),
+ space_id=os.environ["ARIZE_SPACE_ID"],
+ api_key=os.environ["ARIZE_API_KEY"],
+ # Export each span before the process moves on; batch mode is disabled
+ # so the CLI batch command does not need an explicit flush.
+ batch=False,
+ )
+ OpenAIInstrumentor().instrument(tracer_provider=provider)
+ # OITracer adds @chain, which captures the receipt function's input/output.
+ return OITracer(provider.get_tracer(instrumentation_scope), config=TraceConfig())
diff --git a/python/cookbooks/receipt_image_evals/ui.py b/python/cookbooks/receipt_image_evals/ui.py
new file mode 100644
index 0000000..b506304
--- /dev/null
+++ b/python/cookbooks/receipt_image_evals/ui.py
@@ -0,0 +1,64 @@
+"""HTML renderers for the receipt expense inbox."""
+
+from __future__ import annotations
+
+from html import escape
+from typing import Any
+
+EXPENSE_INBOX_HEADER = (
+ 'Expense inbox
'
+ 'Select a submitted receipt and create a structured expense record.
'
+)
+SUBMITTED_RECEIPT_LABEL = 'Submitted receipt
'
+STRUCTURED_RECORD_LABEL = 'Structured expense record
'
+INITIAL_EXPENSE_SUMMARY = (
+ 'Expense details
'
+ '
Process a receipt to create an expense record.
'
+)
+
+
+def render_stat(label: str, value: str) -> str:
+ """Render one labeled value in the expense summary."""
+ return f"{escape(label)}{escape(value)}
"
+
+
+def render_expense_summary(record: dict[str, Any]) -> str:
+ """Render an extracted receipt as an expense summary card."""
+ currency = str(record.get("currency") or "—")
+ merchant = str(record.get("merchant") or "Merchant pending review")
+ total = record.get("total")
+ total_value = f"{currency} {total:,.2f}" if isinstance(total, (int, float)) else "Amount pending review"
+ review_needed = bool(record.get("needs_review", False))
+ review_class = "review-needed" if review_needed else "review-ok"
+ review_text = "Review needed" if review_needed else "Ready for review"
+ stats = "".join(
+ (
+ render_stat("Expense type", "Receipt"),
+ render_stat("Line items", str(len(record.get("items") or []))),
+ render_stat("Currency", currency),
+ )
+ )
+ return (
+ ''
+ f"
{escape(merchant)}
"
+ f'
{escape(total_value)}
'
+ f'
{stats}
'
+ f'
{review_text}'
+ "
"
+ )
+
+
+def render_queue_status(*, total_receipts: int, processed_count: int) -> str:
+ """Render the pending and processed receipt counts."""
+ pending_count = total_receipts - processed_count
+ return f'{pending_count} pending · {processed_count} processed
'
+
+
+def render_processing_status() -> str:
+ """Render successful processing and trace status."""
+ return 'Trace created in AX. · Receipt moved to Processed.
'
+
+
+def render_processing_error(error: Exception) -> str:
+ """Render a safely escaped extraction error."""
+ return f'Processing failed: {escape(str(error))}
'