feat: 98 generative AI agent for classification#105
Conversation
lukeroantreeONS
left a comment
There was a problem hiding this comment.
I've added some comments for the current implementation, but I also think it's worth doing a PoC (maybe in another branch) of a post-processing 'hook' type implementation so we can compare the pros/cons of the two approaches.
| ## | ||
|
|
||
|
|
||
| class Agentase(ABC): |
| "classification": 1 | ||
| } | ||
|
|
||
| The XML structure for the context and user query will be as follows: |
There was a problem hiding this comment.
If we're going with a solid XML structure requirement, should we enforce (some) input sanitisation to ensure user-input doesn't break the XML? (e.g. escape '<')
There was a problem hiding this comment.
Similar concerns with other characters if we opt for a JSON-like approach instead etc.
| top_entries = df.nsmallest(5, "rank") | ||
|
|
||
| # Build the <Context> section | ||
| context_entries = "\n".join( |
There was a problem hiding this comment.
My preference would be to use a dedicated tool for forming the XML, to avoid surprises / edge cases / etc. (e.g. https://pypi.org/project/dicttoxml/)
| classification = validated_response.classification | ||
|
|
||
| # Validate the classification value is in the expected range | ||
| MIN_INDEX = 0 |
There was a problem hiding this comment.
Do we want to lock this restriction in rather than let the user specify a max?
| Guidelines: | ||
| 1. Always prioritize the provided context when making your classification. | ||
| 2. The context will be provided as an XML structure containing multiple entries. Each entry includes an ID and a text description. | ||
| 3. The IDs will be integer values from 0 to 4, corresponding to the 5 candidate entries. |
There was a problem hiding this comment.
Do we want to enforce this range for all use-cases? I think making it user-configurable would be good. Happy to be convinced otherwise though
|
|
||
| else: | ||
| raise ValueError( | ||
| f"Unsupported task_type: {task_type}. Current supported types are 'reranking' and 'classification'." |
There was a problem hiding this comment.
Preference would be to have a class-level list of supported options, which is used to format this error message - centralises where these are defined, helping avoid missing updating the error etc. if we add options down the line.
| f"Unsupported task_type: {task_type}. Current supported types are 'reranking' and 'classification'." | ||
| ) | ||
|
|
||
| def transform(self, results: VectorStoreSearchOutput) -> VectorStoreSearchOutput: |
There was a problem hiding this comment.
It would be good to think early about making this asynchronous - each generative request will be slow, but with little to do on this side while waiting
| ## | ||
|
|
||
|
|
||
| class Agentase(ABC): |
There was a problem hiding this comment.
| class Agentase(ABC): | |
| class AgentBase(ABC): |
There was a problem hiding this comment.
I like this as a demonstration of what's possible with the system. In general though, it feels too specific and 'brittle' to become a fixed part of the package, if you know what I mean? For example, the prompt hard codes the number of results retrieved. Would a more general system have the user constructing a templated prompt, rather than us constructing a templated prompt for the user? What other scaffolding could we give users in constructing RAG post-processors without it becoming too brittle?
I'm more convinced now that this would be better implemented as a post-processing hook, because then we could present it as more of an example and less of a fixed feature?
✨ Summary
These changes introduce the framework for using Generative AI Large Language models to perform classifications on VectorStoreSearchResult objects, and integrates with the existing VectorStore search pipeline.
It is adding a new module called agents, which function similarly to the Vectorisers class/module. Currently implemented is the GcpVectoriser. This class has a
transform()method which intakes aVectorStoreSearchOutputobject and transforms it in some way using generative AI but still outputs a validVectorStoreSearchOutputobject.For example, we can instantiate an agent that classifies the top K semantic search candidates with the following Python code:
The instantiated agent has a
transformmethod that accepts aVectorStoreSearchOutputobject:This standalone agent can then be injected into the VectorStore, so that it automatically runs when the
VectorStore.search()method is called, by setting the 'agent' attribute. Either on instantiation:or by setting the attribute to a VectorStore already in runtime:
my_vector_store.agent = my_agentInner workings and framework design
The GcpVectoriser inherits from a base class that specifies the Agents must take in and output a
VectorStoreSearchOutputobject.This introduces a core concepts of how we use Generative AI in the ClassifAI workflow - i.e. it should only manipulate existing results objects.
There are 4 key components to the GcpAgent model that has been created:
using this 4 step approach makes it possible to create different ways of using the Agent, by changing the system prompt instruction and the corresponding post-processing function - all while grounding the behaviour to make sure we're only manipulating valid VectorStoreSearchOutput objects.
Other agent models such as a HuggingfaceAgent (still to be developed), could follow the same paradigm, and it would give guidance to package users who may wish to implement their own custom generative model by inheriting from the base class.
Classification example
Both the System Prompt and the post-processing function can vary in behaviour based on which task type the agent model is instantiated with. Currently only 'classification' task type is available.
With the classification task type the system prompt is set as (paraphrasing for briefness):
(the actual system prompt is more detailed, contains an example of the data structure, and guidelines - see this in the code files)
The system prompt and formatted search results are combined and passed to the generative model.
The classification post-processing function then assesses if the generative AI output is of the correct format using Pydantic. If it is, it takes the chosen ID and reduces the original VectorStoreSearchOutput object down to the chosen row using the ID generated by the generative AI. If the correct format response was not generated, the post-processing function simply returns the original VectorStoreSearchOutput with no changes.
Considerations:
Agents as hooks instead of as a specific attribute of the VectorStore
One specific consideration, since all the agent is doing is transforming the VectorStoreSearchOutput object into another VectorStoreSearchOutput: it could be treated simply as a post-processing hook on the VectorStore search method. This would be in line with the hooks update we did recently and would simplify the integration of the agent with the VectorStore, although the integration currently is not complex. We could just provide the agents as a kind of fancy pre-build hook ready for use by users.
📜 Changes Introduced
✅ Checklist
terraform fmt&terraform validate)🔍 How to Test
Installing this branch as normal, and then running through the new notebook would be a good way to explore and test the features of the changes. One note, is to make sure to use a google cloud project with the generative language api activated. Otherwise the requests to that api won't be successful