-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feature(dspy): Copro optimizer prompts are now injectable #942
Conversation
Thanks for the PR @tobicoveo ! Could you please rebase and merge this branch with the latest version on main? Seems like there was a failing test on our end (nothing wrong with this PR) that just needs an update. Also, could you add a couple lines of documentation to the Usage Suggestions, specifying what the injectable signatures are defaulted to and what they are used for, now that they are parameters? Ready to merge after that! As an aside, seems like this can be extended to the MIPRO optimizer, if you'd like to tag that along with this PR! :) |
@@ -153,13 +157,13 @@ def compile(self, student, *, trainset, eval_kwargs): | |||
if self.prompt_model: | |||
with dspy.settings.context(lm=self.prompt_model): | |||
instruct = dspy.Predict( | |||
BasicGenerateInstruction, | |||
self.basic_generate_instruction, | |||
n=self.breadth - 1, | |||
temperature=self.init_temperature, | |||
)(basic_instruction=basic_instruction) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These calls to Predict, and later attempts to access members of instruct place pretty strict requirements on the acceptable signatures - maybe more flexible to allow the partial-creation of the entire instruct object?
Think a protocol or the like which specifies the signature of the signature you pass in is probably a must to allow users to understand exactly what they can put in here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @mikeedjones, yeah I agree it was a little bit too loose given the requirements of the signatures. Instead of making the signature injectable, I allowed the user to add instructions and create the signatures from the base class.
Let me know if you see an issue with this approach.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does seem much more sensible - but maybe worth adding a method to dspy.Signature
to allow this sort of user injection of instructions into predefined signatures?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah - that's what "with_instructions" does!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But I want to preserve the initial instructions as well; it would be something more towards the line of append_to_instructions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think it would be more straightforward for users to specify a protocol which defines the signature of an injectable BasicGenerateInstruction
and then leave it to the user to grab the original instructions from this file, if they want to append to the originals?
I think there's a wider discussion on how to manage the internal prompts of dspy and how to enable user access to them?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think down the line, it would make sense to allow the user to change the whole instructions. One could even consider some kind of meta-learning approach on the instructions of the signature used to generate the other prompts (i.e., what signatures lead to the best prompt under a few-shot learning approach).
There should be a discussion about enabling users to access these signatures safely IMO.
I am merely starting to get familiar with this library and wanted to keep the changes very scoped in the context of this PR. 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess the concern would be making a small change here actually alters the interface to Copro, which then has to be supported until a breaking 3.x release. Maybe better to figure out how to more modify the internal prompts more generally, and then apply that here?
dspy/teleprompt/copro_optimizer.py
Outdated
self.basic_generate_instruction = basic_generate_instruction or BasicGenerateInstruction | ||
self.generate_instruction_given_attempts = generate_instruction_given_attempts or GenerateInstructionGivenAttempts |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any benefit to not just having these types as the defaults and making basic_generate_instruction
optional? BasicGenerateInstruction
is a type and shouldn't be getting mutated at runtime.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To avoid mutation of BasicGenerateInstruction
, I encapsulated them into properity methods.
Let me know if it is good with you!
Thanks @arnavsinghvi11!
|
dspy/teleprompt/copro_optimizer.py
Outdated
self.basic_generate_instruction = ( | ||
self._get_signature(BasicGenerateInstruction).with_instructions( | ||
" ".join([BasicGenerateInstruction.instructions, additional_instructions]) | ||
) | ||
if additional_instructions | ||
else BasicGenerateInstruction | ||
) | ||
self.generate_instruction_given_attempts = ( | ||
self._get_signature(GenerateInstructionGivenAttempts).with_instructions( | ||
" ".join([GenerateInstructionGivenAttempts.instructions, additional_instructions]) | ||
) | ||
if additional_instructions | ||
else GenerateInstructionGivenAttempts | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't want to update the fields, but the instructions. Unless I am mistaken, the Signature class doesn't have a method for this.
dspy/teleprompt/copro_optimizer.py
Outdated
@property | ||
def basic_generate_instruction(self): | ||
return (self._get_signature(BasicGenerateInstruction).with_instructions( | ||
" ".join([BasicGenerateInstruction.instructions, self._additional_instructions]) | ||
) | ||
if self._additional_instructions | ||
else BasicGenerateInstruction) | ||
|
||
@property | ||
def generate_instruction_given_attempts(self): | ||
return ( | ||
self._get_signature(GenerateInstructionGivenAttempts).with_instructions( | ||
" ".join([GenerateInstructionGivenAttempts.instructions, self._additional_instructions]) | ||
) | ||
if self._additional_instructions | ||
else GenerateInstructionGivenAttempts | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe can combine to:
@property | |
def basic_generate_instruction(self): | |
return (self._get_signature(BasicGenerateInstruction).with_instructions( | |
" ".join([BasicGenerateInstruction.instructions, self._additional_instructions]) | |
) | |
if self._additional_instructions | |
else BasicGenerateInstruction) | |
@property | |
def generate_instruction_given_attempts(self): | |
return ( | |
self._get_signature(GenerateInstructionGivenAttempts).with_instructions( | |
" ".join([GenerateInstructionGivenAttempts.instructions, self._additional_instructions]) | |
) | |
if self._additional_instructions | |
else GenerateInstructionGivenAttempts | |
) | |
def append_instructions(self, base_signature, additional_instructions): | |
return self._get_signature(base_signature).with_instructions( | |
" ".join([BasicGenerateInstruction.instructions, self._additional_instructions or ""]) | |
) | |
@property | |
def basic_generate_instruction(self): | |
return append_instructions(BasicGenerateInstruction, self._additional_instructions) | |
@property | |
def generate_instruction_given_attempts(self): | |
return append_instructions(GenerateInstructionGivenAttempts, self._additional_instructions) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added the intuitions behind your recommendation. There were a few typos, but I fixed them.
@arnavsinghvi11 and @mikeedjones, is there anything missing to close this PR? |
tagging @XenonMolecule to further confirm the behavior of COPRO and MIPRO. LGTM. |
This looks good to me! Cool idea to allow user-defined grounding for instruction proposal! |
I'm really not sure about this PR. Making a small change here actually alters the interface to Copro, which then has to be supported until a breaking 3.x release. I think this functionality would be better implemented by figuring out how to allow the user to modify any of the internal prompts of dspy, and then apply that same logic here. Does that make sense? |
Sure, I see your point, Michael. Are you suggesting some config or shared interface that lets you swap out the prompts for COPRO, MIPRO, and any other DSPy internal program that uses prompts? This is a good design decision to come up with before we lock into a specific interface too soon. One thing that we've been working on for some internal projects is refactoring MIPRO and COPRO to allow user-defined proposal programs that can include any number of details about your program. This would mean taking into account more than just the Dataset Summary, but also things like the code in your program itself, various prompt engineering tips, etc. Enabling users to plug-in from existing proposal programs or write their own might be a better interface to commit to. |
Yes - there are ~8 I think it would be possible to use something like a context manager from dspy.teleprompt import mipro_optimizer
import dspy
class MyBasicGenerateInstruction(mipro_optimizer.BasicGenerateInstruction):
"you are foo"
basic_instruction = dspy.InputField(desc="The initial instructions before bar")
with mipro_optimizer.BasicGenerateInstruction.replace(MyBasicGenerateInstruction):
print(mipro_optimizer.BasicGenerateInstruction.__doc__)
# you are foo which would require something like #dspy/signatures/signature.py
class Signature(BaseModel, metaclass=SignatureMeta):
"" # noqa: D419
# Note: Don't put a docstring here, as it will become the default instructions
# for any signature that doesn't define it's own instructions.
pass
@classmethod
@contextmanager
def replace(cls, new_signature: Type["Signature"]):
"""Replace the signature with an updated version.
This is useful for updating the internal signatures of dspy
"""
class OldSignature(cls, Signature):
pass
replace_fields = ["__doc__", "model_fields", "model_extra", "model_config"]
for field in replace_fields:
setattr(cls, field, getattr(new_signature, field))
cls.model_rebuild(force=True)
yield
for field in replace_fields:
setattr(cls, field, getattr(OldSignature, field))
cls.model_rebuild(force=True) Then probably want some helper functions so the user can supply a mapping of |
Sketched out in #1090 |
Thanks @mikeedjones, I will close this PR! 🙂 |
not sure everything in my PR is gold! - I would appreciate it if you could check that it satisfies your requirements and stuff? |
I already checked it out, and it looks good to me! My understanding is that I could write the custom signature with additional instructions that I want and use the context manager to replace the base signature. |
What:
Allow additional instruction to be added to the signatures used to generate the prompts.
Why:
Generally speaking, it allows the users more freedom regarding the instructions used to generate the prompts.
In our case, It unluck new possibilities to guide the generating prompt. Optimization based on metrics is already a powerful approach. However, being able to add details regarding how the prompt will be evaluated would help generate prompts that fulfill the requirements that must be filled out without relying only on the LLM to understand what is essential from the metrics score.
How: