Multi-turn session wrapper for DSPy programs — turn any module into a stateful conversation with optimizer-ready linearization.
Project description
dspy-session
dspy-session adds stateful, multi-turn behavior to DSPy modules while staying adapter-agnostic.
It works with:
dspy.Predict/dspy.ChainOfThought- composed
dspy.Moduleprograms with nested predictors - any adapter (
ChatAdapter,JSONAdapter,XMLAdapter,TemplateAdapter, ...)
Install
pip install dspy-session
Why this exists
DSPy already has dspy.History, but it is manual:
history = dspy.History(messages=[...])
out = predictor(question="...", history=history)
dspy-session automates this and adds:
- per-turn state accumulation
- turn snapshots (
history_snapshotat call time) - linearization into optimizer-ready
dspy.Examples - per-turn scoring + filtering
Quickstart (single predictor)
import dspy
from dspy_session import sessionify
class QA(dspy.Signature):
question: str = dspy.InputField()
answer: str = dspy.OutputField()
dspy.configure(lm=dspy.LM("openai/gpt-4o-mini"))
session = sessionify(dspy.Predict(QA))
session(question="What is DSPy?")
session(question="How is it different from plain prompting?")
print(len(session.turns))
print(session.session_history)
What happens:
- Session deep-copies the module
- It ensures predictors have a history input field
- Each call builds a
Historyfrom previous turns - That history is injected into predictor calls
- Turn is recorded with an exact
history_snapshot
Program wrapping (no history kwarg required)
You do not need to modify your program’s forward signature.
import dspy
from dspy_session import sessionify
class QA(dspy.Signature):
question: str = dspy.InputField()
answer: str = dspy.OutputField()
class Agent(dspy.Module):
def __init__(self):
super().__init__()
self.gen = dspy.Predict(QA)
def forward(self, question):
# No history argument here.
return self.gen(question=question)
session = sessionify(Agent())
Internally, dspy-session wraps nested predictors and injects history via context when not explicitly provided.
Linearization for optimizers
Each turn becomes an independent training example:
examples = session.to_examples()
# each example has inputs including history snapshot + output labels
Filtering with a metric
def quality_metric(example, pred, trace=None):
return 1.0 if "good" in pred.answer.lower() else 0.0
good_examples = session.to_examples(metric=quality_metric, min_score=0.5)
Strict trajectory mode
If a bad turn should invalidate all later turns in that session:
strict_examples = session.to_examples(
metric=quality_metric,
min_score=0.5,
strict_trajectory=True,
)
Optimizer workflow
If your examples include history (include_history=True, default), optimize a module/signature that accepts history.
Typical workflow:
# collect turns
session = sessionify(dspy.Predict(QA))
...
trainset = session.to_examples()
# optimize the session-aware module
optimized = dspy.BootstrapFewShot().compile(session, trainset=trainset)
If you want to optimize a non-history base program, use:
trainset = session.to_examples(include_history=False)
History policies for explicit history input
When caller passes history=... explicitly:
history_policy="override"(default): stateless pass-through, no turn recordedhistory_policy="use_if_provided": use provided history for this call and record turnhistory_policy="replace_session": replace session seed history, clear turns, then continue
session = sessionify(my_module, history_policy="use_if_provided")
Controlling what enters history
Sliding window
session = sessionify(my_module, max_turns=10)
Excluding fields (e.g. giant RAG context)
session = sessionify(my_module, exclude_fields={"context"})
Include only selected input fields in history
session = sessionify(my_module, history_input_fields={"question"})
Seed history / resume conversations
seed = dspy.History(messages=[{"question": "Hi", "answer": "Hello!"}])
session = sessionify(my_module, initial_history=seed)
Manual turn editing
session.add_turn(
inputs={"question": "edited question"},
outputs={"answer": "edited answer"},
)
session.pop_turn() # remove last turn
session.undo(steps=2) # remove last 2 turns
Serialization
Save/load session state (module is not serialized):
session.save("session.json")
restored = dspy_session.Session.load_from("session.json", my_module)
Hot-swap optimized module
Keep user conversation state, replace model/program weights:
session.update_module(new_optimized_module)
Async + locks
session = sessionify(my_module, lock="async")
out = await session.acall(question="...")
lock options:
"none"(default)"thread""async"
API reference
sessionify(module, **kwargs) -> Session
Session(
module,
history_field="history",
max_turns=None,
exclude_fields=None,
history_input_fields=None, # alias: input_field_override
initial_history=None,
history_policy="override", # override | use_if_provided | replace_session
on_metric_error="zero", # zero | raise
strict_history_annotation=False,
lock="none", # none | thread | async
)
# calls
session(...)
session.forward(...)
await session.acall(...)
await session.aforward(...)
# state
session.turns
session.session_history
len(session)
session.reset()
session.fork()
# manual editing
session.add_turn(inputs=..., outputs=...)
session.pop_turn()
session.undo(steps=1)
# scoring / examples
session.score(metric, gold=None)
session.to_examples(...)
session.to_trainset(...)
Session.merge_examples(*sessions, ...)
# persistence
session.save(path)
session.save_state()
Session.load_from(path, module)
# module lifecycle
session.update_module(new_module)
Notes
Sessionis adspy.Module, so nested use inside larger DSPy programs remains optimizer-discoverable vianamed_predictors().- For optimizer replay calls that pass explicit history, Session can behave statelessly (
history_policy="override"). - For very long sessions, use
max_turnsand/or strict filtering strategies.
License
MIT
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 dspy_session-0.1.0.tar.gz.
File metadata
- Download URL: dspy_session-0.1.0.tar.gz
- Upload date:
- Size: 20.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ed17a690e632bd516e4820c6d42fc631fc094e9280b761e42281eb61f2e68c60
|
|
| MD5 |
6244690d95d472b75dc61dc09e5eeee5
|
|
| BLAKE2b-256 |
6fe7df4192b10caf5c1a231ab86c9fa8d55dc62e0c1051dc98fd3fdd9f95b04c
|
File details
Details for the file dspy_session-0.1.0-py3-none-any.whl.
File metadata
- Download URL: dspy_session-0.1.0-py3-none-any.whl
- Upload date:
- Size: 19.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dbefd497f36fd2f5054b5d3f1739f795a4d57e7da1bcba5fc6b9b54a7a85f482
|
|
| MD5 |
d275aac8736945d923501d3ae324a0d8
|
|
| BLAKE2b-256 |
d9a9de6bfb5e5b7b153c9d900c827ae93cf544d26d6ef2c3f6abdda26eb88bad
|