Solving Hierarchical List Parsing in Legal Documents (Without LLM Guessing)

When working with legal or regulatory documents (such as EU legislation), one of the deceptively hard problems is correctly modeling hierarchical lists. These documents are full of nested structures like:

  • numbered paragraphs (1., 2.),
  • lettered items ((a), (b)),
  • roman numerals ((i), (ii), (iii)),

often mixed with free-text paragraphs, definitions, and exceptions.

At first glance, this looks simple. In practice, it’s one of the main sources of downstream errors in search, retrieval, and AI-assisted answering.

The Core Problem

HTML representations of legal texts (e.g. EUR-Lex) are structurally inconsistent:

  • nesting depth is not reliable,
  • list items are often rendered using generic <div> grids,
  • numbering may reset visually without resetting the DOM hierarchy,
  • multiple paragraphs can belong to the same logical list item.

If you naïvely chunk text or rely on DOM depth alone, you end up with:

  • definitions split across chunks,
  • list items grouped incorrectly,
  • or worst of all: unrelated provisions merged together.

Once this happens, downstream agents or LLMs are forced to guess structure — which leads to hallucinations, missing conditions, or incorrect legal interpretations.

The Design Goal

The goal was not to “understand” the document using an LLM.

The goal was to:

  • encode the document’s logical structure deterministically at index time, so that:
    • list hierarchy is explicit,
    • grouping is stable,
    • and retrieval can be purely mechanical.

In other words: make the data correct so the AI doesn’t have to be clever.

Continue reading “Solving Hierarchical List Parsing in Legal Documents (Without LLM Guessing)”

From “Table 1” to Searchable Knowledge

A Practical Guide to Handling Large Legal Tables in RAG Pipelines

When working with legal documents—especially EU legislation like EUR-Lex—you quickly run into a hard problem: tables.

Not small tables.
Not friendly tables.
But hundreds-row, multi-page tables buried inside 300+ page PDFs, translated into 20+ languages.

If you are building a Retrieval-Augmented Generation (RAG) system, naïvely embedding these tables almost always fails. You end up with embeddings that contain nothing more than:

“Table 1”

…and none of the actual data users are searching for.

This post describes a production-grade approach to handling large legal tables in a RAG pipeline, based on real issues encountered while indexing EU regulations (e.g. Regulation (EC) No 1333/2008).


The Core Problem

Let’s start with a real example from EUR-Lex:

ANNEX III
PART 6
Table 1 — Definitions of groups of food additives

The table itself contains hundreds of rows like:

  • E 170 — Calcium carbonate
  • E 260 — Acetic acid
  • E 261 — Potassium acetates

What goes wrong in many pipelines

  1. The table heading (“Table 1”) is detected as a section.
  2. The actual <table> element is ignored or stored separately.
  3. Embeddings are generated from the heading text only.

Result:

Embedding text length: 7
Embedding content: "Table 1"

The data exists visually—but not semantically.


Design Goals

We defined a few non-negotiable goals:

  1. The table must be searchable
    Queries like “E170 calcium carbonate” must hit the table.
  2. IDs must be stable and human-readable
    ANNEX_III_PART_6_TABLE_1 is better than _TBL0.
  3. Structured data must be preserved
    We want JSON rows for precise answering, not just text.
  4. Embeddings must stay within limits
    Some tables have hundreds of rows.
Continue reading “From “Table 1” to Searchable Knowledge”

Why General AI Assistants Aren’t Enough: The Case for Domain-Specific Enterprise AI Systems

Over the past two years, the tech landscape has been transformed by generative AI tools like Microsoft Copilot, ChatGPT, Gemini, and others. These assistants have become essential for daily productivity: they summarize documents, write code, answer questions, and drastically improve workflows.

But as soon as organizations begin exploring serious automation of regulated, multi-step, domain-specific processes, one reality becomes clear:

General-purpose AI assistants are not built for high-precision enterprise use cases.

This isn’t a flaw — it’s simply not their mission.
For enterprise-grade scenarios, businesses require specialized, data-aware, multi-agent AI systems designed for accuracy, compliance, and internal knowledge integration.

Here’s why.


1. Data Access ≠ Domain Understanding

Copilot and similar tools can read files from SharePoint, Teams, OneDrive, and other sources.
However, access alone does not create understanding.

General assistants cannot:

  • interpret industry-specific document structures,
  • follow multi-step regulatory logic,
  • understand cross-referenced obligations,
  • map documents across markets or jurisdictions,
  • align internal and external rules,
  • or execute deterministic procedures.

They are trained for broad, generic reasoning — not domain-structured reasoning.

Domain-specific enterprise AI systems, in contrast, are built to:

  • model relationships between documents,
  • extract structured information,
  • classify data reliably,
  • apply rule-based logic,
  • and reason across heterogeneous sources.

2. Enterprise AI Requires Traceability — Not Just an Answer

General AI models work probabilistically: they return the most likely answer.

Enterprise workflows demand something different:

  • exact citations,
  • section and paragraph references,
  • version and source transparency,
  • reproducibility,
  • evidence of reasoning,
  • strict alignment with regulatory text.

Productivity assistants cannot guarantee any of these.
Enterprise AI must — especially in domains such as:

  • compliance,
  • legal obligations,
  • regulatory affairs,
  • quality assurance,
  • product safety,
  • documentation governance.

Without traceability, AI cannot operate in regulated environments.

Continue reading “Why General AI Assistants Aren’t Enough: The Case for Domain-Specific Enterprise AI Systems”

Continuing with the Microsoft Agent Framework – Practical Examples 06–10 for Real-World Development

A few weeks ago, I published an article titled
Getting Started with the Microsoft Agent Framework and Azure OpenAI – My First Five .NET 9 Samples,
where I walked through the foundational building blocks of the Agent Framework—basic agents, tool invocation, streaming output, and structured responses.

This article is the direct continuation of that series.

While my current work focuses on much more advanced multi-agent systems—where layered knowledge processing, document pipelines, and context-routing play a crucial role—I still believe that the best way to understand a new technology is through clear, minimal, practical samples.

So in this follow-up, I want to share the next set of examples that helped me understand the deeper capabilities of the framework.
These are Examples 06–10, and they focus on persistence, custom storage, observability, dependency injection, and MCP tool hosting.

Let’s dive in.


Example 06 — Persistent Threads: Saving & Restoring Long-Running Conversations

One of the major strengths of the Microsoft Agent Framework is the ability to maintain conversational state across messages, sessions, and even application restarts.
This is critical if you’re building:

  • personal assistants
  • developer copilots
  • chat interfaces
  • multi-step reasoning chains
  • or anything that needs user-specific memory

In Example 06, the agent:

  1. Starts a new conversation thread
  2. Answers a developer-related question
  3. Serializes the thread using thread.Serialize()
  4. Saves the serialized state to disk (or any storage)
  5. Reloads it later
  6. Resumes the conversation with full continuity

Why this matters:

  • Enables long-lived, multi-turn conversations
  • You can manage per-user memory in your own storage
  • Perfect for web apps, bots, and multi-agent orchestration
  • Unlocks real “assistant-like” behavior

This is the first step toward user-level persistent AI.

Continue reading “Continuing with the Microsoft Agent Framework – Practical Examples 06–10 for Real-World Development”

Building a .NET AI Chat App with Microsoft Agent Framework and Aspire Orchestration

Creating a fully functional AI Chat App today doesn’t have to take weeks.
With the new Microsoft Agent Framework and .NET Aspire orchestration, you can set up a complete, observable, and extensible AI solution in just a few minutes — all running locally, with built-in monitoring and Azure OpenAI integration.

If you’ve experimented with modern chat applications, you’ve probably noticed they all share a similar design.
So instead of reinventing the wheel, we’ll leverage the elegant Blazor-based front end included in Microsoft’s AI templates — and focus our energy where it matters most: the intelligence and orchestration behind it.

But where things get truly exciting is behind the scenes — where you can move from a simple chat client to a structured, observable AI system powered by Microsoft Agent Framework and .NET Aspire orchestration.

Why Use the Agent Framework?

The Microsoft Agent Framework brings much-needed architectural depth to your AI solutions. It gives you:

  • Separation of concerns – keep logic and tools outside UI components
  • Testability – verify agent reasoning and tool behavior independently
  • Advanced reasoning – support for multi-step decision flows
  • Agent orchestration – easily coordinate multiple specialized agents
  • Deep observability – gain insight into every AI operation and decision

Essentially, it lets you transform a plain chat app into an intelligent, composable system.

Why .NET Aspire Makes It Even Better

One of the best parts about using the AI templates is that everything runs through .NET Aspire.
That gives you:

  • Service discovery between components
  • Unified logging and telemetry in the Aspire dashboard
  • Health checks for every service
  • Centralized configuration for secrets, environment variables, and connection settings

With Aspire, you get orchestration, observability, and consistency across your entire local or cloud-ready environment — no extra setup required.

Continue reading “Building a .NET AI Chat App with Microsoft Agent Framework and Aspire Orchestration”

Building an AI Chat app with .NET Aspire, Ollama/OpenAI, Postgres, and Redis

With .NET Aspire, you can orchestrate a full AI chat system — backend, model, data store, and frontend — from one place.
This sample shows how Aspire can manage a large language model (LLM), a Postgres conversation database, a Redis message broker, and a React-based chat UI, all within a single orchestration file.

Folder layout

11_AIChat/
├─ AppHost/ # Aspire orchestration
├─ ChatApi/ # .NET 9 backend API (SignalR + EF)
├─ chatui/ # React + Vite frontend
├─ ServiceDefaults/ # shared settings (logging, health, OTEL)
└─ README.md

Overview

This example demonstrates:

  • AI model orchestration with local Ollama or hosted OpenAI
  • Postgres database for conversation history
  • Redis for live chat streaming and cancellation coordination
  • Chat API using ASP.NET Core + SignalR
  • React/Vite frontend for real-time conversations
  • Full Docker Compose publishing via Aspire

The AppHost (orchestration)

AppHost/Program.cs

var builder = DistributedApplication.CreateBuilder(args);

// Publish this as a Docker Compose application
builder.AddDockerComposeEnvironment("env")
       .WithDashboard(db => db.WithHostPort(8085))
       .ConfigureComposeFile(file =>
       {
           file.Name = "aspire-ai-chat";
       });

// The AI model definition
var model = builder.AddAIModel("llm");

if (OperatingSystem.IsMacOS())
{
    model.AsOpenAI("gpt-4o-mini");
}
else
{
    model.RunAsOllama("phi4", c =>
    {
        c.WithGPUSupport();
        c.WithLifetime(ContainerLifetime.Persistent);
    })
    .PublishAsOpenAI("gpt-4o-mini");
}

// Postgres for conversation history
var pgPassword = builder.AddParameter("pg-password", secret: true);

var db = builder.AddPostgres("pg", password: pgPassword)
                .WithDataVolume(builder.ExecutionContext.IsPublishMode ? "pgvolume" : null)
                .WithPgAdmin()
                .AddDatabase("conversations");

// Redis for message streams + coordination
var cache = builder.AddRedis("cache").WithRedisInsight();

// Chat API service
var chatapi = builder.AddProject<Projects.ChatApi>("chatapi")
                     .WithReference(model).WaitFor(model)
                     .WithReference(db).WaitFor(db)
                     .WithReference(cache).WaitFor(cache);

// Frontend served via Vite
builder.AddNpmApp("chatui", "../chatui")
       .WithNpmPackageInstallation()
       .WithHttpEndpoint(env: "PORT")
       .WithReverseProxy(chatapi.GetEndpoint("http"))
       .WithExternalHttpEndpoints()
       .WithOtlpExporter()
       .WithEnvironment("BROWSER", "none");

builder.Build().Run();
Continue reading “Building an AI Chat app with .NET Aspire, Ollama/OpenAI, Postgres, and Redis”

Running a Python MCP server inside .NET Aspire

.NET Aspire isn’t limited to .NET microservices — you can orchestrate Python components just as easily.
In this post, we’ll look at how to run a Python MCP (Model Context Protocol) server inside an Aspire application, integrate it with the MCP Inspector, and expose it via a Dev Tunnel for testing.

Folder Layout

05_PythonMCP/
├─ AppHost.cs
├─ PythonMCP.csproj
├─ appsettings.json
├─ appsettings.Development.json
├─ NuGet.config
├─ README.md
└─ pymcp/
   ├─ main.py
   ├─ requirements.txt
   └─ (optional) .venv/

AppHost.cs

Here’s the full orchestration file:

var builder = DistributedApplication.CreateBuilder(args);

var inspector = builder.AddMcpInspector("mcp-inspector");

var tunnel = builder.AddDevTunnel("dev-tunnel");

var pyMcpServer = builder.AddPythonApp(
    name: "pymcp",
    appDirectory: "./pymcp",
    scriptPath: "main.py"
).WithHttpEndpoint(env: "PORT");

tunnel.WithReference(pyMcpServer, allowAnonymous: true);

inspector.WithMcpServer(pyMcpServer);

builder. Build().Run();
Continue reading “Running a Python MCP server inside .NET Aspire”

Website Powered by WordPress.com.

Up ↑