LLM Fundamentals in C#
Summary
A supplied judgment can be summarized only from passages you pass in. The same client, with no passage, names a citation that the catalog rejects.
Companion code: 01-llm-fundamentals in indian-legal-ai-lab.
A chat model will write a confident case brief from a pasted judgment. It will also write one when you forget to paste the judgment. The second brief is the failure this lab exists to show.
What you will be able to do
- Put a judgment excerpt into a chat request and get a summary that repeats only that excerpt.
- Run the same client with no excerpt and see an unsupported citation.
- Check that citation against the catalog with `CitationChecker`.
How the call is shaped
`GroundedChatClient` implements `IChatClient` from Microsoft.Extensions.AI. The lab does not call a hosted model. The class is a stand-in so the same method shape (`GetResponseAsync`) can later point at a real provider without changing the caller.
Tokens and the context window
A token is the unit a model counts. A context window is how many tokens fit in one request, including the system text, the judgment, and the answer. This lab does not call a tokenizer, so it does not print a token count. What it does show is the practical rule: if the text you trust is not inside the request, the model is free to answer from training memory.
The lab keeps the trusted text explicit. `Program.Main` loads Romesh Thappar (`1950 INSC 14`), takes the first 400 characters, and prefixes the line:
var excerpt = thappar.Text![..400].Replace('\n', ' ');
var passage = "PASSAGE: " + thappar.Citation + " " + excerpt;
var grounded = await client.GetResponseAsync(
[new ChatMessage(ChatRole.User, "Summarize.\n" + passage)]);That string is the entire context the client is allowed to treat as a source.
What the client does with the request
`GetResponseAsync` joins every message, then branches.
var joined = string.Join("\n", messages.Select(m => m.Text ?? ""));
if (AdviceGuard.IsDisallowed(joined))
{
return Task.FromResult(new ChatResponse(new ChatMessage(
ChatRole.Assistant,
ProductBoundary.Notice + " This question asks for advice or a prediction and will not be answered.")));
}
var passages = ExtractPassages(joined);`AdviceGuard` refuses questions such as "Will I win my case?". `ExtractPassages` keeps only lines that start with `PASSAGE:`.
If that list is empty, the client returns a fixed sentence that names `FICTIONAL 2099 SYN 5` and states that no holding is given. If the list has lines, it copies at most four of them under the educational notice and adds: "Interpretations of these passages require human review."
The checker beside the answer
`CitationChecker.UnknownCitations` builds a set from each record's citation, case id, and neutral citation. It then finds `FICTIONAL yyyy SYN n` in the answer. A match that is not in the set is unknown.
var pattern = Regex.Matches(text, @"FICTIONAL\s+\d{4}\s+SYN\s+\d+");
foreach (Match match in pattern)
{
if (!known.Contains(match.Value))
found.Add(match.Value);
}The ungrounded sentence is written so this check fails on purpose. A real catalog citation inside a `PASSAGE:` line is known and is not flagged.
Run it
cd indian-legal-ai-lab
dotnet run --project labs/01-llm-fundamentalsThe grounded path prints the notice and the Thappar excerpt, including `[1950] 1 S.C.R. 594`. The bare path prints that ungrounded mode has no source passages. `Ungrounded_client_does_not_invent_a_known_holding` asserts that `FICTIONAL 2099 SYN 5` is reported as unknown.
What this does not prove
The client is deterministic. It does not measure fluency, and it does not show how a hosted model would behave with the same prompt. The pattern to keep is the contract: passages in, restatement out, citations checked against the catalog.
Live provider, not used by the default lab
`OpenAiCompatibleChatClient` posts to `{LEGALAI_CHAT_BASE_URL}/v1/chat/completions`. A unit test feeds it a fake HTTP 200 and checks that it reads `choices[0].message.content`. No Ollama or hosted model was running when the tests were recorded, so the lab command still uses `GroundedChatClient`. That class is the offline stub. The sentence that names `FICTIONAL 2099 SYN 5` is text the stub returns so the checker has a fixture. It is not an observed hallucination from a model.
Next
Part 2: Simple RAG over Indian Court Judgments
Verified in this revision
`OpenAiCompatibleChatClient.GetResponseAsync` posts `tools` when `ChatOptions.Tools` is set and copies `tool_calls` into `FunctionCallContent`. `GetStreamingResponseAsync` reads SSE lines that start with `data:` and yields each `delta.content`. `Chat_client_streams_deltas_and_reads_tool_calls` covers both with a stub `HttpMessageHandler`. No live model was called. `CitationChecker.Assess` reports `ExistsInCatalog` and `SupportedByPassage` separately. A catalog hit with no passage text is existence, not support.
More in Applied AI
Simple RAG over Indian Court Judgments
Chunk a versioned judgment manifest, embed it locally, and return the passage with its character offsets.
Advanced Legal Retrieval
Keyword, hashing-vector, and hybrid search on the same question, plus the SQL a pgvector store would run.
Function Calling and Legal Research Tools
Deterministic C# tools for case metadata, passages, and a short constitutional catalog, with rejected blank input.