Expect scripts for LLM conversations
Project description
expectllm
Expect scripts for LLM conversations.
The insight: Agents are just expect scripts. Send a message, expect a pattern, branch on the match. That's it.
Table of Contents
- Why expectllm?
- Requirements
- Installation
- Quick Start
- Before/After
- Features
- API Reference
- Examples
- Environment Variables
- Prompting Tips
- Important Notes
- Contributing
- License
Why expectllm?
- Zero boilerplate — No chains, no schemas, no output parsers. Just send and expect.
- Pattern-first design — Use regex patterns you already know. The LLM adapts to your format, not the other way around.
- Conversation as state machine — Each expect is a transition. Branch on match, retry on failure, build complex flows naturally.
- Provider agnostic — Works with OpenAI, Anthropic, or any compatible API. Switch providers without changing code.
- Debuggable — Every step is visible. No hidden prompts, no magic. Print the history, see what happened.
- Lightweight — Single file, minimal dependencies. No framework lock-in.
- Unix philosophy — Do one thing well. Compose with your existing tools.
Requirements
- Python 3.9+
- API key for at least one provider (OpenAI or Anthropic)
Installation
# Core only (no providers)
pip install expectllm
# With specific provider
pip install expectllm[openai]
pip install expectllm[anthropic]
# All providers
pip install expectllm[all]
Quick Start
from expectllm import Conversation
c = Conversation()
c.send("Is Python dynamically typed? Reply YES or NO")
if c.expect_yesno():
print("Correct!")
That's it. Send a message, expect a pattern, branch on the result.
Before/After
Traditional approach (20+ lines):
from langchain.chat_models import ChatOpenAI
from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
# ... setup, chains, error handling ...
expectllm (4 lines):
from expectllm import Conversation
c = Conversation()
c.send("Review this code. Reply with SEVERITY: low/medium/high",
expect=r"SEVERITY: (low|medium|high)")
severity = c.match.group(1)
Features
Pattern-to-Prompt
When you pass an expect pattern to send(), expectllm automatically appends format instructions:
# You write:
c.send("Is this secure?", expect=r"^(YES|NO)$")
# expectllm sends:
# "Is this secure?
#
# Reply with exactly 'YES' or 'NO'."
Expect Templates
No regex needed for common patterns:
# Extract JSON
c.send("Return user data as JSON")
data = c.expect_json() # Returns dict
# Extract numbers
c.send("How many items?")
count = c.expect_number() # Returns int
# Yes/No questions
c.send("Is this valid? Reply YES or NO")
if c.expect_yesno(): # Returns bool
print("Valid!")
# Multiple choice
c.send("Classify as: bug, feature, or docs")
category = c.expect_choice(["bug", "feature", "docs"])
# Extract code
c.send("Write a Python function")
code = c.expect_code("python") # Returns code string
API Reference
Conversation
c = Conversation(
model="claude-sonnet-4-20250514", # Optional, auto-detected from env
system_prompt="You are helpful", # Optional
timeout=60, # Default timeout in seconds
provider="anthropic", # Optional: "openai" or "anthropic"
max_history=20 # Optional: limit conversation history
)
Methods
| Method | Returns | Description |
|---|---|---|
send(message, expect=None) |
str |
Send message, optionally validate pattern |
expect(pattern, flags=0) |
bool |
Match pattern in last response |
send_expect(message, pattern) |
Match |
Send and expect in one call |
expect_json() |
dict |
Extract JSON from response |
expect_number() |
int |
Extract first number |
expect_choice(choices) |
str |
Match one of the choices |
expect_yesno() |
bool |
Match yes/no variants |
expect_code(language=None) |
str |
Extract code block |
clear_history() |
None |
Clear conversation history |
Properties
| Property | Type | Description |
|---|---|---|
match |
Match | None |
Last successful match object |
history |
List[Dict] |
Conversation history (copy) |
last_response |
str |
Most recent response |
Exceptions
| Exception | When |
|---|---|
ExpectError |
Pattern not found in response |
ProviderError |
API call failed |
ConfigError |
Missing API key or invalid config |
Examples
Extract Structured Data
from expectllm import Conversation
c = Conversation()
c.send("Parse this: 'Meeting with John at 3pm tomorrow'")
event = c.expect_json() # {"person": "John", "time": "3pm", "date": "tomorrow"}
Multi-Turn Code Review
from expectllm import Conversation, ExpectError
import re
code = '''
def process_user(data):
query = f"SELECT * FROM users WHERE id = {data['id']}"
return db.execute(query)
'''
c = Conversation()
c.send(f"Review this code for security issues:\n```python\n{code}\n```")
c.expect(r"(found (\d+) issues|no issues found)", re.IGNORECASE)
if c.match.group(2) and int(c.match.group(2)) > 0:
c.send("List the issues with severity ratings")
c.expect(r"(critical|high|medium|low)", re.IGNORECASE)
print(c.last_response)
Data Extraction
from expectllm import Conversation
text = "Contact John Smith at john@example.com or 555-1234"
c = Conversation()
c.send(f"""Extract contact info from:
{text}
Format:
NAME: <name>
EMAIL: <email>
PHONE: <phone>""")
c.expect(r"NAME: (.+)\nEMAIL: (.+)\nPHONE: (.+)")
print(f"Name: {c.match.group(1)}")
print(f"Email: {c.match.group(2)}")
print(f"Phone: {c.match.group(3)}")
Retry Pattern
from expectllm import Conversation, ExpectError
def analyze_document(text: str, max_retries: int = 3) -> dict:
c = Conversation(system_prompt="You are a document analyzer.")
c.send(f"Analyze this document and extract key entities:\n\n{text}")
for attempt in range(max_retries):
try:
return c.expect_json()
except ExpectError:
if attempt < max_retries - 1:
c.send("Please format your response as valid JSON.")
raise ExpectError("Failed to extract JSON after retries")
Environment Variables
Set your API key:
# For Anthropic (Claude)
export ANTHROPIC_API_KEY="your-key"
# For OpenAI (GPT)
export OPENAI_API_KEY="your-key"
expectllm auto-detects the provider from the environment. Anthropic is preferred if both are set.
Prompting Tips
For reliable pattern matching:
- Be explicit about format: "Reply with exactly 'YES' or 'NO'"
- Use examples: "Format: SCORE: 8/10"
- Constrain output: "Reply with just the number, nothing else"
- Use code blocks: "Put your JSON in a ```json code block"
Important Notes
LLM Non-Determinism: LLM outputs are inherently non-deterministic. The same prompt may produce different responses across calls. For production use:
- Use explicit format instructions (expectllm does this automatically with
expect=) - Implement retry logic for critical extractions
- Consider temperature=0 for more consistent outputs
- Test patterns against varied response formats
Not a Testing Framework: expectllm is for scripting conversations, not for unit testing LLM outputs. For assertions about LLM behavior, combine with your existing test framework.
Contributing
See CONTRIBUTING.md for development setup and guidelines.
License
MIT License - see LICENSE for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file expectllm-0.1.0.tar.gz.
File metadata
- Download URL: expectllm-0.1.0.tar.gz
- Upload date:
- Size: 9.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aadae0b047eda8334b0fec6f9bd329b784e08c6d7c5739d7dce5713385985d5b
|
|
| MD5 |
d6bdbcdebac85ebe397e471c2b7caee1
|
|
| BLAKE2b-256 |
8059efec393132d45e49e958eab0f00b42bd9f6be0ba1221a18f322353cc5444
|
Provenance
The following attestation bundles were made for expectllm-0.1.0.tar.gz:
Publisher:
publish.yml on entropyvector/expectllm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
expectllm-0.1.0.tar.gz -
Subject digest:
aadae0b047eda8334b0fec6f9bd329b784e08c6d7c5739d7dce5713385985d5b - Sigstore transparency entry: 972391271
- Sigstore integration time:
-
Permalink:
entropyvector/expectllm@97571cd7aed76a832932da82826bcc66195b6614 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/entropyvector
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@97571cd7aed76a832932da82826bcc66195b6614 -
Trigger Event:
release
-
Statement type:
File details
Details for the file expectllm-0.1.0-py3-none-any.whl.
File metadata
- Download URL: expectllm-0.1.0-py3-none-any.whl
- Upload date:
- Size: 10.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2bf3dceb22cde9de139bbff7ca16544a6d245e3730656e97361e56a368be94c4
|
|
| MD5 |
9ec6fa5b16a42659e254e20552061472
|
|
| BLAKE2b-256 |
91f2d5278c0b05612c14255cdf2609de9e80b34cd45ae7f1acb5b414163150c1
|
Provenance
The following attestation bundles were made for expectllm-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on entropyvector/expectllm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
expectllm-0.1.0-py3-none-any.whl -
Subject digest:
2bf3dceb22cde9de139bbff7ca16544a6d245e3730656e97361e56a368be94c4 - Sigstore transparency entry: 972391278
- Sigstore integration time:
-
Permalink:
entropyvector/expectllm@97571cd7aed76a832932da82826bcc66195b6614 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/entropyvector
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@97571cd7aed76a832932da82826bcc66195b6614 -
Trigger Event:
release
-
Statement type: