Skip to main content

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
  • 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

Project details


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.1.tar.gz (22.5 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.1-py3-none-any.whl (21.3 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: dspy_session-0.1.1.tar.gz
  • Upload date:
  • Size: 22.5 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.1.tar.gz
Algorithm Hash digest
SHA256 74212b9b526f8fb5c213cf99fbde7114ec96bd22ace64625531aa508dbd7f564
MD5 897f174324eab8ad065b7efeecd4e007
BLAKE2b-256 87fabd250c7c1a18a5e62d3161c6ef882b6681eb9bef584ce4b128cc186dca13

See more details on using hashes here.

File details

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

File metadata

  • Download URL: dspy_session-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 21.3 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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 cb33e256ed98b24fcaba440ebdd7cc696833775fba929e543245ae59dfa18d44
MD5 a80037fff3b1509cdea6dbe2860a6c8e
BLAKE2b-256 aadc583605d75c9025ef3c20160da64ee483f84f706ed4492abec69d653d63f4

See more details on using hashes here.

Supported by

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