autogen icon indicating copy to clipboard operation
autogen copied to clipboard

Builtin method for Azure Cognitive Search?

Open tyler-suard-parker opened this issue 7 months ago • 10 comments

I noticed there was a module for searching Qdrant, it would be nice to have one too for Azure Cognitive Search.

tyler-suard-parker avatar Nov 20 '23 21:11 tyler-suard-parker

It might be as easy as adding the right configuration in the llm_config because I remember seeing a similar question which uses Azure Cognitive Search in an API compatible with openai inference.

sonichi avatar Dec 03 '23 18:12 sonichi

I would also be very happy for such solution. I can't find any documentation related with Azure Cognitive Search and AutoGen.

joao-d-oliveira avatar Dec 07 '23 14:12 joao-d-oliveira

Agreed - would be great to have an example of how to use AutoGen with Azure Cognitive/AI Search. Not finding any documentation on it. I think we might be able to create a custom implementation of the RetrieveUserProxyAgent like they did with Qdrant. But given this is a microsoft backed project, I imagine a dedicated Azure AI Search implementation should be on the roadmap

blakedwbrownie avatar Jan 18 '24 19:01 blakedwbrownie

I'm surprised this isn't built into Autogen already, is anyone actively working on adding Azure AI Search RAG Agents?

Adding the completions extensions API for DataSources to the llm_config could help to implement this. https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#azure-ai-search

Sample app example for datasources extensions - https://github.com/microsoft/sample-app-aoai-chatGPT/blob/main/app.py

config_list = [ { "model": "gpt-35-turbo-16k", "api_key": os.environ.get("AZURE_OPENAI_API_KEY"), "api_base": os.environ.get("AZURE_OPENAI_ENDPOINT"), "api_type": "azure", "api_version": "2023-05-15", "dataSources": [ { "type": "AzureCognitiveSearch", "parameters": { "endpoint": f"https://{AZURE_SEARCH_SERVICE}.search.windows.net", "key": AZURE_SEARCH_KEY, "indexName": AZURE_SEARCH_INDEX, "fieldsMapping": { "contentFields": parse_multi_columns(AZURE_SEARCH_CONTENT_COLUMNS) if AZURE_SEARCH_CONTENT_COLUMNS else [], "titleField": AZURE_SEARCH_TITLE_COLUMN if AZURE_SEARCH_TITLE_COLUMN else None, "urlField": AZURE_SEARCH_URL_COLUMN if AZURE_SEARCH_URL_COLUMN else None, "filepathField": AZURE_SEARCH_FILENAME_COLUMN if AZURE_SEARCH_FILENAME_COLUMN else None, "vectorFields": parse_multi_columns(AZURE_SEARCH_VECTOR_COLUMNS) if AZURE_SEARCH_VECTOR_COLUMNS else [] }, "inScope": True if AZURE_SEARCH_ENABLE_IN_DOMAIN.lower() == "true" else False, "topNDocuments": int(AZURE_SEARCH_TOP_K), "queryType": query_type, "semanticConfiguration": AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG if AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG else "", "roleInformation": AZURE_OPENAI_SYSTEM_MESSAGE, "filter": filter, "strictness": int(AZURE_SEARCH_STRICTNESS) } } ] }
]

bsonnek avatar Feb 02 '24 14:02 bsonnek

I've got it working, but I needed to do some minor changes (I may submit a PR if i have time)

If anyone is interested the basic steps are:

  • add the dataSources section to "extra_body" in llm_config
  • add "/extensions" to the model name, this is hacky but it means the correct completions url is used

Those 2 steps should create the correct request but the response.choices causes an error, it expects a Message value to check for any function_call's but this is null and instead the message data is in Messages. I created a AzureCognitiveSearchAgent agent and override generate_oai_reply with a fix in there but there may be a better way.

Config Examples

config_list = [ { 'model': "[MODEL_NAME]/extensions", 'api_key': apiKey, 'base_url': base_url, 'api_type': apiType, 'api_version': search_api_version, } ]

llm_config={ "timeout": 600, "cache_seed": 41, "config_list": config_list, "extra_body":{ "dataSources":[ { "type": "AzureCognitiveSearch", "parameters": { "endpoint": "https://[SEARCH_NAME].search.windows.net", "indexName": "[INDEX_NAME]", "key": "[SEARCH_KEY]" } } ] } },

m-carter1 avatar Feb 02 '24 15:02 m-carter1

@m-carter1 Please summit MR

cforce avatar Feb 03 '24 16:02 cforce

Is it only for retrieval argumented agent? Would you consider adding the support in the retrieval_utils.py, and creating an AgentCapability so RAG can be added to any agent?

ekzhu avatar Feb 03 '24 16:02 ekzhu

Is it only for retrieval argumented agent? Would you consider adding the support in the retrieval_utils.py, and creating an AgentCapability so RAG can be added to any agent?

This is a functionality from Azure Cognitive Search. It has an API compatible to Azure OpenAI. Any ConversableAgent can use the llm_config to access Azure Cognitive Search. So I don't think a AgentCapability is necessary here. But documentation, examples are going to be helpful. cc @jackgerrits @m-carter1 @thinkall

sonichi avatar Feb 11 '24 17:02 sonichi

Are there any examples meanwhile? @sonichi @m-carter1 @jackgerrits @thinkall

cforce avatar Mar 03 '24 08:03 cforce

Looking for a solution for Azure AI search, too.

chengyuliu-msft avatar Mar 18 '24 12:03 chengyuliu-msft

Here is suggested changes:

1- Config list needs to follow the following format: config_list = [{ "model": "gpt-35-turbo", "api_key": "***", "base_url": "https://<***>.openai.azure.com/", "api_type": "azure", "api_version": "2024-02-15-preview", "extra_body": { "data_sources": [ { "type": "AzureCognitiveSearch", "parameters": { "endpoint": "https://<***>.search.windows.net", "indexName": "<***>", "key": "<***>", "semanticConfiguration":"<***>", "queryType":"semantic", "fieldsMapping": { "contentFieldsSeparator": "\n", "contentFields": ["<***>"], "filepathField": "<***>", "titleField": "<***>", "urlField": "<***>", "vectorFields": [] }, "filter": null, "inScope": true, "strictness": 3, "topNDocuments": 5 } } ] } } ]

2-Modify message_retrieval in oai Client:

` def message_retrieval( self, response: Union[ChatCompletion, Completion] ) -> Union[List[str], List[ChatCompletionMessage]]: """Retrieve the messages from the response.""" choices = response.choices if isinstance(response, Completion): return [choice.text for choice in choices] # type: ignore [union-attr]

    if TOOL_ENABLED:
        return [  # type: ignore [return-value]
            (
                choice.message  # type: ignore [union-attr] 
                if choice.message.function_call is not None or choice.message.tool_calls is not None  # type: ignore [union-attr]
                else self._format_message_with_citations(choice) #choice.message.content
            )  # type: ignore [union-attr]
            for choice in choices
        ]
    else:
        return [  # type: ignore [return-value]
            choice.message if choice.message.function_call is not None else self._format_message_with_citations(choice)  # type: ignore [union-attr]
            for choice in choices
        ]

`

3- add the following function to oai Client:

`@staticmethod def _format_message_with_citations(choice: List[Dict[str, Any]]) -> str: message_content = choice.message.content try: citations = choice.message.context.get('citations', []) if citations: citation_texts = [] for i, citation in enumerate(citations, start=1): doc_ref = f"[doc{i}]" doc_title = citation.get('title', 'Document Title not provided') doc_path = citation.get('url', 'URL/File path not provided') doc_content = citation.get('content', 'Content not provided') citation_text = f"{i}. [{doc_title}] ({doc_path})\n{doc_content}" citation_texts.append(citation_text)

                # Replace inline citations with doc references
                message_content = message_content.replace(citation['filepath'], doc_ref)
            
            return f"{message_content}\n\nCitations:\n" + "\n\n".join(citation_texts)
    except Exception:
        return choice.message.content`

4-Create a new agent: `import copy from typing import Dict, List, Optional, Tuple, Union

from autogen import OpenAIWrapper from autogen.agentchat import Agent, ConversableAgent

from autogen.code_utils import content_str

from ..._pydantic import model_dump

DEFAULT_LMM_SYS_MSG = """You are a helpful AI assistant.""" DEFAULT_MODEL = "gpt-35-turbo"

class AzureCognitiveSearchAgent(ConversableAgent): DEFAULT_CONFIG = { "model": DEFAULT_MODEL, }

def __init__(
    self,
    name: str,
    system_message: Optional[Union[str, List]] = DEFAULT_LMM_SYS_MSG,
    is_termination_msg: str = None,
    *args,
    **kwargs,
):
    """
    Args:
        name (str): agent name.
        system_message (str): system message for the OpenAIWrapper inference.
            Please override this attribute if you want to reprogram the agent.
        **kwargs (dict): Please refer to other kwargs in
            [ConversableAgent](../conversable_agent#__init__).
    """
    super().__init__(
        name,
        system_message,
        is_termination_msg=is_termination_msg,
        *args,
        **kwargs,
    )
    # call the setter to handle special format.
    self.update_system_message(system_message)
    self._is_termination_msg = (
        is_termination_msg
        if is_termination_msg is not None
        else (lambda x: content_str(x.get("content")) == "TERMINATE")
    )

    # Override the `generate_oai_reply`
    self.replace_reply_func(ConversableAgent.generate_oai_reply, AzureCognitiveSearchAgent.generate_oai_reply)
    self.replace_reply_func(
        ConversableAgent.a_generate_oai_reply,
        AzureCognitiveSearchAgent.a_generate_oai_reply,
    )

def update_system_message(self, system_message: Union[Dict, List, str]):
    """Update the system message.

    Args:
        system_message (str): system message for the OpenAIWrapper inference.
    """
    self._oai_system_message[0]["content"] = self._message_to_dict(system_message)["content"]
    self._oai_system_message[0]["role"] = "system"

def generate_oai_reply(
    self,
    messages: Optional[List[Dict]] = None,
    sender: Optional[Agent] = None,
    config: Optional[OpenAIWrapper] = None,
) -> Tuple[bool, Union[str, Dict, None]]:
    """Generate a reply using autogen.oai."""
    client = self.client if config is None else config
    if client is None:
        return False, None
    if messages is None:
        messages = self._oai_messages[sender]

    # TODO: #1143 handle token limit exceeded error
    response = client.create(context=messages[-1].pop("context", None), messages=messages)

    # TODO: line 301, line 271 is converting messages to dict. Can be removed after ChatCompletionMessage_to_dict is merged.
    extracted_response = client.extract_text_or_completion_object(response)[0]
    
    if not isinstance(extracted_response, str):
        try:
            extracted_response = model_dump(extracted_response)
        except Exception as e:
            extracted_response = "The requested information is not available in the retrieved data. Please try another query or topic."

    return True, extracted_response

`


create an AzureCognitiveSearchAgent instance named "retrieval_agent"

retrieval_agent = AzureCognitiveSearchAgent( name="retrieval_agent", llm_config=gpt35_rag_llm_config, )

9hsein5 avatar Apr 07 '24 01:04 9hsein5

@9hsein5 do you want to work together on a PR for adding your code to Autogen?

tyler-suard-parker avatar Apr 08 '24 16:04 tyler-suard-parker

@9hsein5 do you want to work together on a PR for adding your code to Autogen?

Yes let's go with it

9hsein5 avatar Apr 08 '24 16:04 9hsein5

@sonichi Do you have any recommendations for how we can implement this according to @9hsein5 's code? Do you want us to create a protocol/interface etc? What file or folder would you recommend we add this to?

tyler-suard-parker avatar Apr 08 '24 18:04 tyler-suard-parker