Skip to content
Merged
134 changes: 133 additions & 1 deletion dspy/signatures/signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,31 @@ class Signature(BaseModel, metaclass=SignatureMeta):

@classmethod
def with_instructions(cls, instructions: str) -> type["Signature"]:
"""Return a new Signature class with identical fields and new instructions.

This method does not mutate `cls`. It constructs a fresh Signature
class using the current fields and the provided `instructions`.

Args:
instructions (str): Instruction text to attach to the new signature.

Returns:
A new Signature class whose fields match `cls.fields`
and whose instructions equal `instructions`.

Example:
```python
import dspy

class MySig(dspy.Signature):
input_text: str = dspy.InputField(desc="Input text")
output_text: str = dspy.OutputField(desc="Output text")

NewSig = MySig.with_instructions("Translate to French.")
assert NewSig is not MySig
assert NewSig.instructions == "Translate to French."
```
"""
return Signature(cls.fields, instructions)

@classmethod
Expand Down Expand Up @@ -275,14 +300,87 @@ def with_updated_fields(cls, name: str, type_: type | None = None, **kwargs: dic

@classmethod
def prepend(cls, name, field, type_=None) -> type["Signature"]:
"""Insert a field at index 0 of the `inputs` or `outputs` section.

Args:
name (str): Field name to add.
field: `InputField` or `OutputField` instance to insert.
type_ (type | None): Optional explicit type annotation. If `type_` is `None`, the effective type is
resolved by `insert`.

Returns:
A new `Signature` class with the field inserted first.

Example:
```python
import dspy

class MySig(dspy.Signature):
input_text: str = dspy.InputField(desc="Input sentence")
output_text: str = dspy.OutputField(desc="Translated sentence")

NewSig = MySig.prepend("context", dspy.InputField(desc="Context for translation"))
print(list(NewSig.fields.keys()))
```
"""
return cls.insert(0, name, field, type_)

@classmethod
def append(cls, name, field, type_=None) -> type["Signature"]:
"""Insert a field at the end of the `inputs` or `outputs` section.

Args:
name (str): Field name to add.
field: `InputField` or `OutputField` instance to insert.
type_ (type | None): Optional explicit type annotation. If `type_` is `None`, the effective type is
resolved by `insert`.

Returns:
A new Signature class with the field appended.

Example:
```python
import dspy

class MySig(dspy.Signature):
input_text: str = dspy.InputField(desc="Input sentence")
output_text: str = dspy.OutputField(desc="Translated sentence")

NewSig = MySig.append("confidence", dspy.OutputField(desc="Translation confidence"))
print(list(NewSig.fields.keys()))
```
"""
return cls.insert(-1, name, field, type_)

@classmethod
def delete(cls, name) -> type["Signature"]:
"""Return a new Signature class without the given field.

If `name` is not present, the fields are unchanged (no error raised).

Args:
name (str): Field name to remove.

Returns:
A new Signature class with the field removed (or unchanged if the field was absent).

Example:
```python
import dspy

class MySig(dspy.Signature):
input_text: str = dspy.InputField(desc="Input sentence")
temp_field: str = dspy.InputField(desc="Temporary debug field")
output_text: str = dspy.OutputField(desc="Translated sentence")

NewSig = MySig.delete("temp_field")
print(list(NewSig.fields.keys()))

# No error is raised if the field is not present
Unchanged = NewSig.delete("nonexistent")
print(list(Unchanged.fields.keys()))
```
"""
fields = dict(cls.fields)

fields.pop(name, None)
Expand All @@ -291,6 +389,38 @@ def delete(cls, name) -> type["Signature"]:

@classmethod
def insert(cls, index: int, name: str, field, type_: type | None = None) -> type["Signature"]:
"""Insert a field at a specific position among inputs or outputs.

Negative indices are supported (e.g., `-1` appends). If `type_` is omitted, the field's
existing `annotation` is used; if that is missing, `str` is used.

Args:
index (int): Insertion position within the chosen section; negatives append.
name (str): Field name to add.
field: InputField or OutputField instance to insert.
type_ (type | None): Optional explicit type annotation.

Returns:
A new Signature class with the field inserted.

Raises:
ValueError: If `index` falls outside the valid range for the chosen section.

Example:
```python
import dspy

class MySig(dspy.Signature):
input_text: str = dspy.InputField(desc="Input sentence")
output_text: str = dspy.OutputField(desc="Translated sentence")

NewSig = MySig.insert(0, "context", dspy.InputField(desc="Context for translation"))
print(list(NewSig.fields.keys()))

NewSig2 = NewSig.insert(-1, "confidence", dspy.OutputField(desc="Translation confidence"))
print(list(NewSig2.fields.keys()))
```
"""
# It's possible to set the type as annotation=type in pydantic.Field(...)
# But this may be annoying for users, so we allow them to pass the type
if type_ is None:
Expand Down Expand Up @@ -430,7 +560,9 @@ class MyType:
# program of thought and teleprompters, so we just silently default to string.
if type_ is None:
type_ = str
if not isinstance(type_, (type, typing._GenericAlias, types.GenericAlias, typing._SpecialForm, types.UnionType)):
if not isinstance(
type_, (type, typing._GenericAlias, types.GenericAlias, typing._SpecialForm, types.UnionType)
):
raise ValueError(f"Field types must be types, but received: {type_} of type {type(type_)}.")
if not isinstance(field, FieldInfo):
raise ValueError(f"Field values must be Field instances, but received: {field}.")
Expand Down