LangChain (Python) LCEL chain with structured parsing, tested with a fake chat model: working versions (tested 2026-10-15)
Compose ChatPromptTemplate, a chat model and a Pydantic output parser with LCEL in langchain-core, tested offline using GenericFakeChatModel.
passed5 passed · 0 failed2026-10-15 13:31 UTC- Status
- passed
- Last passed
- 2026-10-15 13:31 UTC
- Tests
- 5 / 5
- Language
- Python
- Runtime
- Python 3.13.16
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38742087302
The code
Exactly the files that passed. Your agent can pull them with get_recipe("langchain-core-fake-llm-python") (install), or see them on GitHub.
langchain-core==1.6.10 pydantic==2.14.1 pytest==9.1.1
from langchain_core.language_models import BaseChatModel
from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.prompts import ChatPromptTemplate
from pydantic import BaseModel, Field
class Sentiment(BaseModel):
label: str = Field(pattern="^(positive|negative|neutral)$")
score: float = Field(ge=0, le=1)
parser = PydanticOutputParser(pydantic_object=Sentiment)
prompt = ChatPromptTemplate.from_messages([
("system", "Classify sentiment.\n{format_instructions}"),
("human", "{text}"),
]).partial(format_instructions=parser.get_format_instructions())
def build_chain(model: BaseChatModel):
return prompt | model | parser
import pytest
from langchain_core.exceptions import OutputParserException
from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
from langchain_core.messages import AIMessage
from chain import Sentiment, build_chain, prompt
def fake(*replies):
return GenericFakeChatModel(messages=iter([AIMessage(content=r) for r in replies]))
def test_parses_structured_output():
out = build_chain(fake('{"label": "positive", "score": 0.9}')).invoke({"text": "love it"})
assert out == Sentiment(label="positive", score=0.9)
def test_parses_fenced_json():
out = build_chain(fake('```json\n{"label": "neutral", "score": 0.5}\n```')).invoke({"text": "ok"})
assert out.label == "neutral"
def test_bad_output_raises():
with pytest.raises(OutputParserException):
build_chain(fake('{"label": "angry", "score": 3}')).invoke({"text": "grr"})
def test_prompt_includes_format_instructions():
msgs = prompt.format_messages(text="hi")
assert "score" in msgs[0].content and msgs[1].content == "hi"
def test_batch():
outs = build_chain(fake('{"label":"positive","score":1}', '{"label":"negative","score":0.1}')).batch([{"text": "a"}, {"text": "b"}], config={"max_concurrency": 1})
assert [o.label for o in outs] == ["positive", "negative"]
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
FastAPI + Pydantic v2 validators, discriminated unions and serializers
Pydantic v2 patterns in FastAPI that agents often get wrong: field_validator, model_validator, discriminated unions, computed_field and response_model filtering.
FastAPI background tasks + dependency overrides
FastAPI BackgroundTasks for post-response work, with dependency_overrides to swap services in tests.
FastAPI endpoint with Pydantic v2 discriminated unions and Annotated validators
Accept a tagged union of payment methods with Field(discriminator), add Annotated validators, and test the exact 422 error shapes offline with TestClient.
httpx async API client + respx mocks
A typed async API client on httpx with retries on 5xx and Pydantic parsing, tested offline with respx route mocks.