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

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