Skip to content
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

Add config classes #591

Merged
merged 89 commits into from
Feb 12, 2024
Merged

Add config classes #591

merged 89 commits into from
Feb 12, 2024

Conversation

collindutter
Copy link
Member

@collindutter collindutter commented Jan 18, 2024

Closes #242, #509

This PR introduces the following changes:

Added

  • OpenAiStructureConfig for providing Structures with all OpenAi Driver configuration.
  • AmazonBedrockStructureConfig for providing Structures with all Amazon Bedrock Driver configuration.
  • StructureConfig for building your own Structure configuration.
  • JsonExtractionTask for convenience over using ExtractionTask with a JsonExtractionEngine.
  • CsvExtractionTask for convenience over using ExtractionTask with a CsvExtractionEngine.

Changed

  • Deprecated Structure.prompt_driver in favor of Structure.global_drivers.prompt_driver.
  • Deprecated Structure.embedding_driver in favor of Structure.global_drivers.embedding_driver.
  • Deprecated Structure.stream in favor of Structure.global_drivers.prompt_driver.stream.
  • TextSummaryTask.summary_engine now defaults to a PromptSummaryEngine with a Prompt Driver default of Structure.global_drivers.prompt_driver.
  • TextQueryTask.query_engine now defaults to a VectorQueryEngine with a Prompt Driver default of Structure.global_drivers.prompt_driver and Vector Store Driver default of Structure.global_drivers.vector_store_driver.
  • PromptImageGenerationTask.image_generation_engine now defaults to a PromptImageGenerationEngine with an Image Generation Driver default of Structure.global_drivers.image_generation_driver.
  • VariationImageGenerationTask.image_generation_engine now defaults to a VariationImageGenerationEngine with an Image Generation Driver default of Structure.global_drivers.image_generation_driver.
  • InpaintingImageGenerationTask.image_generation_engine now defaults to an InpaintingImageGenerationEngine with an Image Generation Driver default of Structure.global_drivers.image_generation_driver.
  • OutpaintingImageGenerationTask.image_generation_engine now defaults to an OutpaintingImageGenerationEngine with an Image Generation Driver default of Structure.global_drivers.image_generation_driver.

Some usage patterns that come from this:

Configuring a Structure to use Bedrock:

agent = Agent(
    config=AmazonBedrockStructureConfig()
)

Configuring a Structure to use Anthropic:

agent = Agent(
    config=StructureConfig(
        global_drivers=StructureGlobalDriversConfig(
            prompt_driver=AnthropicPromptDriver(
                model="claude-2",
                api_key=os.environ["ANTHROPIC_API_KEY"],
            )
        )
    ),
)

Overriding a Structure's Task Memory Query Engine's Embedding Driver:

agent = Agent(
    config=StructureConfig(
        task_memory=StructureTaskMemoryConfig(
            query_engine=StructureTaskMemoryQueryEngineConfig(
                vector_store_driver=LocalVectorStoreDriver(
                    embedding_driver=OpenAiEmbeddingDriver(),
                )
            )
        )
    )
)

Modifying an existing config + serializing/deserializing:

custom_config = AmazonBedrockStructureConfig()
custom_config.global_drivers.embedding_driver = AmazonBedrockCohereEmbeddingDriver()
custom_config.merge_config(
    {
        "task_memory": {
            "summary_engine": {
                "prompt_driver": {
                    "model": "amazon.titan-text-express-v1",
                    "prompt_model_driver": {
                        "type": "BedrockTitanPromptModelDriver",
                    },
                }
            }
        }
    }
)
serialized_config = custom_config.to_json()
deserialized_config = AmazonBedrockStructureConfig.from_json(serialized_config)

agent = Agent(
    config=deserialized_config,
)

@andrewfrench
Copy link
Member

@collindutter #635 updated the TextQueryTask to depend on VectorQueryEngine rather than BaseQueryEngine but only added top_n to VectorQueryEngine. Your change here still references BaseQueryEngine, as it probably should, so I added top_n to the base class in #640

@andrewfrench
Copy link
Member

Code changes look great and after playing with the features this is a clear huge step forward. One question @collindutter is that while Tasks look up to Structures to inherit configurations (like Engines), Tools don't do this. Tools also don't have an existing reference to a Structure that they can look to, but is this intentional? It seems like a natural convenience (even if in a later PR) that an Agent, for example, is provided with a configuration and the developer doesn't have to specifically re-wire its Tools.

@collindutter
Copy link
Member Author

Thanks for the feedback and for trying it out! Yes this is something that Tools should also have since they're basically a sibling abstraction to Tasks. I only extended the functionality here because it was already partially implemented in Prompt Task with a look up to Prompt Driver. Agreed that in a future PR we should evaluate how we can better share logic between Tasks and Tools.

andrewfrench
andrewfrench previously approved these changes Feb 8, 2024
Copy link
Member

@andrewfrench andrewfrench left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Big fan, can't wait to type less.

Copy link
Member

@vasinov vasinov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome work! Should make it way easier to use and extend the framework!


@define(kw_only=True)
class AmazonBedrockStructureConfig(BaseStructureConfig):
global_drivers: StructureGlobalDriversConfig = field(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we rename it to global and StructureGlobalConfig in case we add more things, other than drivers, to it in the future?

Or should we keep it namespaced to drivers for clarity and just introduce another config "bundle" in the future?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to keep it namespaced; I could see a singular global becoming quite unruly.

)


@define(kw_only=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we apply this to all new classes in this PR and, in a separate PR, to most (or all?) classes in Griptape?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd actually vote to remove it from this PR, and do it in a single sweep in another.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, let's remove it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@define(kw_only=True)
class StructureGlobalDriversConfig(SerializableMixin):
prompt_driver: BasePromptDriver = field(
kw_only=True, default=Factory(lambda: NopPromptDriver()), metadata={"serializable": True}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wanted to make sure this is the best convention...do we like NoOp, Null, Dummy, Stub, or Empty better? Or should we stick with Nop?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like Stub. Nop and NoOp feel a bit technical, and Null feels dangerous.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think @andrewfrench?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Traditionally I use Dummy

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good with me too. Before I do a big refactor, @vasinov are you also good with Dummy? DummyPromptDriver?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That works! It's more fun than Stub and Stub is too Java-y anyways :)



@define(kw_only=True)
class StructureTaskMemoryExtractionEngineConfig(SerializableMixin):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Random thought: will we ever want to use configs as mixins? In other words, will it ever make sense to inherit from two configs at once? If that's the case, should we add the Mixin suffix to config files and @define(slots=False)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having a difficult time imaging a configuration situation where we'd inherit from multiple. Did you have something specific in mind?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing specific, just wondering. Let's keep it the way it is then :)

class NopImageGenerationDriver(BaseImageGenerationDriver):
model: str = field(init=False)

def try_text_to_image(self, prompts: list[str], negative_prompts: Optional[list[str]] = None) -> ImageArtifact:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it was established in another PR but why do we prefix those driver methods with try_?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because they have parent methods that wrap them in extra logic. Just following the pattern of other Drivers like try_run in Prompt Driver.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it—makes sense!

Literal["256x256"] | Literal["512x512"] | Literal["1024x1024"] | Literal["1024x1792"] | Literal["1792x1024"]
) = field(default="1024x1024", kw_only=True)
response_format: Literal["b64_json"] = field(default="b64_json", kw_only=True)
Union[Literal["256x256"], Literal["512x512"], Literal["1024x1024"], Literal["1024x1792"], Literal["1792x1024"]]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we back to Union over | everywhere in the framework?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to use Union for class fields as long as we're supporting python 3.9. We had a slack thread on this somewhere I can try to dig up.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes, I remember now.

Comment on lines 82 to 85
if self.structure:
self.structure.publish_event(CompletionChunkEvent(token=chunk.value))
else:
raise ValueError("Streaming requires a structure")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels a bit off: shouldn't prompt drivers still be able to stream regardless of the presence of the structure?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but in the current implementation, publishing events requires a Structure to be set.

I can revert this change since it's kind of out of scope of an already massive PR, but I think we need a separate issue to decouple Prompt Drivers and Structures. Maybe Prompt Drivers take an Event Listener instead.

EDIT: separate issue

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Let's address it in the separate issue.

def default_config(self) -> BaseStructureConfig:
config = OpenAiStructureConfig()

if self.prompt_driver is not None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use a more pythonic @deprecated decorator for those things:

@property
@deprecated("use `config.global_drivers.prompt_driver` instead.")
def prompt_driver(self) -> Optional[BasePromptDriver]:
    return None

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a deprecated decorator and refactored.

Comment on lines 161 to 162
if not isinstance(task_memory.query_engine.prompt_driver, NopPromptDriver)
else global_drivers.prompt_driver
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we flip it here and below to not use a negative condition, so it's easier to read and reason about?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -15,7 +15,9 @@
class BaseTextInputTask(RuleMixin, BaseTask, ABC):
DEFAULT_INPUT_TEMPLATE = "{{ args[0] }}"

_input: str | TextArtifact | Callable[[BaseTask], TextArtifact] = field(default=DEFAULT_INPUT_TEMPLATE)
_input: str | TextArtifact | Callable[[BaseTask], TextArtifact] = field(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No Union here? :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of Union is really only required if the field is marked as serializable. Related to this comment.

@collindutter collindutter merged commit c87c85f into dev Feb 12, 2024
6 checks passed
@collindutter collindutter deleted the feature/default-config branch February 12, 2024 20:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Defaults management
5 participants