Skip to main content

dspy-session

dspy-session adds stateful, multi-turn behavior to DSPy modules while staying adapter-agnostic.

It works with:

  • dspy.Predict / dspy.ChainOfThought
  • dspy.ProgramOfThought
  • dspy.ReAct
  • dspy.CodeAct
  • composed dspy.Module programs 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_snapshot at 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:

  1. Session deep-copies the module
  2. It ensures predictors have a history input field
  3. Each call builds a History from previous turns
  4. That history is injected into predictor calls
  5. Turn is recorded with an exact history_snapshot

TemplateAdapter integration

dspy-session and dspy-template-adapter work extremely well together:

  • dspy-session handles state/history lifecycle
  • TemplateAdapter gives exact prompt-layout control

👉 See the full integration guide (including contrived layouts like "all history in system", "all in user", and split strategies):

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 recorded
  • history_policy="use_if_provided": use provided history for this call and record turn
  • history_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)

Cap in-memory turn storage (for long-running services)

session = sessionify(my_module, max_turns=10, max_stored_turns=200)
# max_turns controls prompt history window
# max_stored_turns controls retained session.turns in memory

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,
    max_stored_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,
    copy_mode="deep",              # deep | shallow | none
    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

  • Session is a dspy.Module, so nested use inside larger DSPy programs remains optimizer-discoverable via named_predictors().
  • For optimizer replay calls that pass explicit history, Session can behave statelessly (history_policy="override").
  • For very long sessions, use max_turns and max_stored_turns to control prompt and memory growth.
  • If your module is not deepcopy-friendly, use copy_mode="shallow" or copy_mode="none".

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dspy_session-0.1.2.tar.gz (22.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dspy_session-0.1.2-py3-none-any.whl (21.8 kB view details)

Uploaded Python 3

File details

Details for the file dspy_session-0.1.2.tar.gz.

File metadata

  • Download URL: dspy_session-0.1.2.tar.gz
  • Upload date:
  • Size: 22.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.3

File hashes

Hashes for dspy_session-0.1.2.tar.gz
Algorithm Hash digest
SHA256 387a13948d37efd4bc56e8d6e7e5e0703273a6c3c38dafc7768c4cf4c1771577
MD5 716a7b4e5cb02a951c81a4f7c41396fc
BLAKE2b-256 365e15ad8c17b40aee70c303f4157ee2fb5d200107e69dc132d03a3d0e357bd1

See more details on using hashes here.

File details

Details for the file dspy_session-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: dspy_session-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 21.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.3

File hashes

Hashes for dspy_session-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 cc9fd4a2530df5c9e00fa504e2f923e9d8d4de7b244d3b829b9c5857e01e53bd
MD5 24006ea6064532eada02d8a7b694d83d
BLAKE2b-256 aac234d3ccc1c4e6354ce417a0d4a874d9bdee56c8b436408be89a2f15adcefb

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.2 This release

2 files

0.1.1

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page