Spaces:
Runtime error
Runtime error
Upload 4 files
Browse files- planning/autogen_planner.py +150 -0
- plugins/bing_connector.py +112 -0
- plugins/sk_bing_plugin.py +38 -0
- plugins/sk_web_pages_plugin.py +35 -0
planning/autogen_planner.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Microsoft. All rights reserved.
|
| 2 |
+
|
| 3 |
+
from typing import Optional
|
| 4 |
+
import semantic_kernel, autogen
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class AutoGenPlanner:
|
| 8 |
+
"""(Demo) Semantic Kernel planner using Conversational Programming via AutoGen.
|
| 9 |
+
|
| 10 |
+
AutoGenPlanner leverages OpenAI Function Calling and AutoGen agents to solve
|
| 11 |
+
a task using only the Plugins loaded into Semantic Kernel. SK Plugins are
|
| 12 |
+
automatically shared with AutoGen, so you only need to load the Plugins in SK
|
| 13 |
+
with the usual `kernel.import_skill(...)` syntax. You can use native and
|
| 14 |
+
semantic functions without any additional configuration. Currently the integration
|
| 15 |
+
is limited to functions with a single string parameter. The planner has been
|
| 16 |
+
tested with GPT 3.5 Turbo and GPT 4. It always used 3.5 Turbo with OpenAI,
|
| 17 |
+
just for performance and cost reasons.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
import datetime
|
| 21 |
+
from typing import List, Dict
|
| 22 |
+
|
| 23 |
+
ASSISTANT_PERSONA = f"""Only use the functions you have been provided with.
|
| 24 |
+
Do not ask the user to perform other actions than executing the functions.
|
| 25 |
+
Use the functions you have to find information not available.
|
| 26 |
+
Today's date is: {datetime.date.today().strftime("%B %d, %Y")}.
|
| 27 |
+
Reply TERMINATE when the task is done.
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
def __init__(self, kernel: semantic_kernel.Kernel, llm_config: Dict = None):
|
| 31 |
+
"""
|
| 32 |
+
Args:
|
| 33 |
+
kernel: an instance of Semantic Kernel, with plugins loaded.
|
| 34 |
+
llm_config: a dictionary with the following keys:
|
| 35 |
+
- type: "openai" or "azure"
|
| 36 |
+
- openai_api_key: OpenAI API key
|
| 37 |
+
- azure_api_key: Azure API key
|
| 38 |
+
- azure_deployment: Azure deployment name
|
| 39 |
+
- azure_endpoint: Azure endpoint
|
| 40 |
+
"""
|
| 41 |
+
super().__init__()
|
| 42 |
+
self.kernel = kernel
|
| 43 |
+
self.llm_config = llm_config
|
| 44 |
+
|
| 45 |
+
def create_assistant_agent(self, name: str, persona: str = ASSISTANT_PERSONA) -> autogen.AssistantAgent:
|
| 46 |
+
"""
|
| 47 |
+
Create a new AutoGen Assistant Agent.
|
| 48 |
+
Args:
|
| 49 |
+
name (str): the name of the agent
|
| 50 |
+
persona (str): the LLM system message defining the agent persona,
|
| 51 |
+
in case you want to customize it.
|
| 52 |
+
"""
|
| 53 |
+
return autogen.AssistantAgent(name=name, system_message=persona, llm_config=self.__get_autogen_config())
|
| 54 |
+
|
| 55 |
+
def create_user_agent(
|
| 56 |
+
self, name: str, max_auto_reply: Optional[int] = None, human_input: Optional[str] = "ALWAYS"
|
| 57 |
+
) -> autogen.UserProxyAgent:
|
| 58 |
+
"""
|
| 59 |
+
Create a new AutoGen User Proxy Agent.
|
| 60 |
+
Args:
|
| 61 |
+
name (str): the name of the agent
|
| 62 |
+
max_auto_reply (int): the maximum number of consecutive auto replies.
|
| 63 |
+
default to None (no limit provided).
|
| 64 |
+
human_input (str): the human input mode. default to "ALWAYS".
|
| 65 |
+
Possible values are "ALWAYS", "TERMINATE", "NEVER".
|
| 66 |
+
(1) When "ALWAYS", the agent prompts for human input every time a message is received.
|
| 67 |
+
Under this mode, the conversation stops when the human input is "exit",
|
| 68 |
+
or when is_termination_msg is True and there is no human input.
|
| 69 |
+
(2) When "TERMINATE", the agent only prompts for human input only when a termination message is received or
|
| 70 |
+
the number of auto reply reaches the max_consecutive_auto_reply.
|
| 71 |
+
(3) When "NEVER", the agent will never prompt for human input. Under this mode, the conversation stops
|
| 72 |
+
when the number of auto reply reaches the max_consecutive_auto_reply or when is_termination_msg is True.
|
| 73 |
+
"""
|
| 74 |
+
return autogen.UserProxyAgent(
|
| 75 |
+
name=name,
|
| 76 |
+
human_input_mode=human_input,
|
| 77 |
+
max_consecutive_auto_reply=max_auto_reply,
|
| 78 |
+
function_map=self.__get_function_map(),
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
def __get_autogen_config(self):
|
| 82 |
+
"""
|
| 83 |
+
Get the AutoGen LLM and Function Calling configuration.
|
| 84 |
+
"""
|
| 85 |
+
if self.llm_config:
|
| 86 |
+
if self.llm_config["type"] == "openai":
|
| 87 |
+
if not self.llm_config["openai_api_key"] or self.llm_config["openai_api_key"] == "sk-...":
|
| 88 |
+
raise Exception("OpenAI API key is not set")
|
| 89 |
+
return {
|
| 90 |
+
"functions": self.__get_function_definitions(),
|
| 91 |
+
"config_list": [{"model": "gpt-3.5-turbo", "api_key": self.llm_config["openai_api_key"]}],
|
| 92 |
+
}
|
| 93 |
+
if self.llm_config["type"] == "azure":
|
| 94 |
+
if (
|
| 95 |
+
not self.llm_config["azure_api_key"]
|
| 96 |
+
or not self.llm_config["azure_deployment"]
|
| 97 |
+
or not self.llm_config["azure_endpoint"]
|
| 98 |
+
):
|
| 99 |
+
raise Exception("Azure OpenAI API configuration is incomplete")
|
| 100 |
+
return {
|
| 101 |
+
"functions": self.__get_function_definitions(),
|
| 102 |
+
"config_list": [
|
| 103 |
+
{
|
| 104 |
+
"model": self.llm_config["azure_deployment"],
|
| 105 |
+
"api_type": "azure",
|
| 106 |
+
"api_key": self.llm_config["azure_api_key"],
|
| 107 |
+
"api_base": self.llm_config["azure_endpoint"],
|
| 108 |
+
"api_version": "2023-08-01-preview",
|
| 109 |
+
}
|
| 110 |
+
],
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
raise Exception("LLM type not provided, must be 'openai' or 'azure'")
|
| 114 |
+
|
| 115 |
+
def __get_function_definitions(self) -> List:
|
| 116 |
+
"""
|
| 117 |
+
Get the list of function definitions for OpenAI Function Calling.
|
| 118 |
+
"""
|
| 119 |
+
functions = []
|
| 120 |
+
sk_functions = self.kernel.skills.get_functions_view()
|
| 121 |
+
for ns in {**sk_functions.native_functions, **sk_functions.semantic_functions}:
|
| 122 |
+
for f in sk_functions.native_functions[ns]:
|
| 123 |
+
functions.append(
|
| 124 |
+
{
|
| 125 |
+
"name": f.name,
|
| 126 |
+
"description": f.description,
|
| 127 |
+
"parameters": {
|
| 128 |
+
"type": "object",
|
| 129 |
+
"properties": {
|
| 130 |
+
f.parameters[0].name: {
|
| 131 |
+
"description": f.parameters[0].description,
|
| 132 |
+
"type": f.parameters[0].type_,
|
| 133 |
+
}
|
| 134 |
+
},
|
| 135 |
+
"required": [f.parameters[0].name],
|
| 136 |
+
},
|
| 137 |
+
}
|
| 138 |
+
)
|
| 139 |
+
return functions
|
| 140 |
+
|
| 141 |
+
def __get_function_map(self) -> Dict:
|
| 142 |
+
"""
|
| 143 |
+
Get the function map for AutoGen Function Calling.
|
| 144 |
+
"""
|
| 145 |
+
function_map = {}
|
| 146 |
+
sk_functions = self.kernel.skills.get_functions_view()
|
| 147 |
+
for ns in {**sk_functions.native_functions, **sk_functions.semantic_functions}:
|
| 148 |
+
for f in sk_functions.native_functions[ns]:
|
| 149 |
+
function_map[f.name] = self.kernel.skills.get_function(f.skill_name, f.name)
|
| 150 |
+
return function_map
|
plugins/bing_connector.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Microsoft. All rights reserved.
|
| 2 |
+
|
| 3 |
+
import urllib, aiohttp
|
| 4 |
+
from logging import Logger
|
| 5 |
+
from typing import Any, List, Optional
|
| 6 |
+
from semantic_kernel.connectors.search_engine.connector import ConnectorBase
|
| 7 |
+
from semantic_kernel.utils.null_logger import NullLogger
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class BingConnector(ConnectorBase):
|
| 11 |
+
"""
|
| 12 |
+
A search engine connector that uses the Bing Search API to perform a web search.
|
| 13 |
+
The connector can be used to read "answers" from Bing, when "snippets" are available,
|
| 14 |
+
or simply to retrieve the URLs of the search results.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
_api_key: str
|
| 18 |
+
|
| 19 |
+
def __init__(self, api_key: str, logger: Optional[Logger] = None) -> None:
|
| 20 |
+
self._api_key = api_key
|
| 21 |
+
self._logger = logger if logger else NullLogger()
|
| 22 |
+
|
| 23 |
+
if not self._api_key:
|
| 24 |
+
raise ValueError("Bing API key cannot be null. Please set environment variable BING_API_KEY.")
|
| 25 |
+
|
| 26 |
+
async def search_url_async(self, query: str, num_results: str, offset: str) -> List[str]:
|
| 27 |
+
"""
|
| 28 |
+
Returns the search results URLs of the query provided by Bing web search API.
|
| 29 |
+
Returns `num_results` results and ignores the first `offset`.
|
| 30 |
+
|
| 31 |
+
:param query: search query
|
| 32 |
+
:param num_results: the number of search results to return
|
| 33 |
+
:param offset: the number of search results to ignore
|
| 34 |
+
:return: list of search results
|
| 35 |
+
"""
|
| 36 |
+
data = await self.__search(query, num_results, offset)
|
| 37 |
+
if data:
|
| 38 |
+
pages = data["webPages"]["value"]
|
| 39 |
+
self._logger.info(pages)
|
| 40 |
+
result = list(map(lambda x: x["url"], pages))
|
| 41 |
+
self._logger.info(result)
|
| 42 |
+
return result
|
| 43 |
+
else:
|
| 44 |
+
return []
|
| 45 |
+
|
| 46 |
+
async def search_snippet_async(self, query: str, num_results: str, offset: str) -> List[str]:
|
| 47 |
+
"""
|
| 48 |
+
Returns the search results Text Preview (aka snippet) of the query provided by Bing web search API.
|
| 49 |
+
Returns `num_results` results and ignores the first `offset`.
|
| 50 |
+
|
| 51 |
+
:param query: search query
|
| 52 |
+
:param num_results: the number of search results to return
|
| 53 |
+
:param offset: the number of search results to ignore
|
| 54 |
+
:return: list of search results
|
| 55 |
+
"""
|
| 56 |
+
data = await self.__search(query, num_results, offset)
|
| 57 |
+
if data:
|
| 58 |
+
pages = data["webPages"]["value"]
|
| 59 |
+
self._logger.info(pages)
|
| 60 |
+
result = list(map(lambda x: x["snippet"], pages))
|
| 61 |
+
self._logger.info(result)
|
| 62 |
+
return result
|
| 63 |
+
else:
|
| 64 |
+
return []
|
| 65 |
+
|
| 66 |
+
async def __search(self, query: str, num_results: str, offset: str) -> Any:
|
| 67 |
+
"""
|
| 68 |
+
Returns the search response of the query provided by pinging the Bing web search API.
|
| 69 |
+
Returns the response content
|
| 70 |
+
|
| 71 |
+
:param query: search query
|
| 72 |
+
:param num_results: the number of search results to return
|
| 73 |
+
:param offset: the number of search results to ignore
|
| 74 |
+
:return: response content or None
|
| 75 |
+
"""
|
| 76 |
+
if not query:
|
| 77 |
+
raise ValueError("query cannot be 'None' or empty.")
|
| 78 |
+
|
| 79 |
+
if not num_results:
|
| 80 |
+
num_results = 1
|
| 81 |
+
if not offset:
|
| 82 |
+
offset = 0
|
| 83 |
+
|
| 84 |
+
num_results = int(num_results)
|
| 85 |
+
offset = int(offset)
|
| 86 |
+
|
| 87 |
+
if num_results <= 0:
|
| 88 |
+
raise ValueError("num_results value must be greater than 0.")
|
| 89 |
+
if num_results >= 50:
|
| 90 |
+
raise ValueError("num_results value must be less than 50.")
|
| 91 |
+
|
| 92 |
+
if offset < 0:
|
| 93 |
+
raise ValueError("offset must be greater than 0.")
|
| 94 |
+
|
| 95 |
+
self._logger.info(
|
| 96 |
+
f"Received request for bing web search with \
|
| 97 |
+
params:\nquery: {query}\nnum_results: {num_results}\noffset: {offset}"
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
_base_url = "https://api.bing.microsoft.com/v7.0/search"
|
| 101 |
+
_request_url = f"{_base_url}?q={urllib.parse.quote_plus(query)}&count={num_results}&offset={offset}"
|
| 102 |
+
|
| 103 |
+
self._logger.info(f"Sending GET request to {_request_url}")
|
| 104 |
+
|
| 105 |
+
headers = {"Ocp-Apim-Subscription-Key": self._api_key}
|
| 106 |
+
|
| 107 |
+
async with aiohttp.ClientSession() as session:
|
| 108 |
+
async with session.get(_request_url, headers=headers, raise_for_status=True) as response:
|
| 109 |
+
if response.status == 200:
|
| 110 |
+
return await response.json()
|
| 111 |
+
else:
|
| 112 |
+
return None
|
plugins/sk_bing_plugin.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Microsoft. All rights reserved.
|
| 2 |
+
|
| 3 |
+
from semantic_kernel.skill_definition import sk_function
|
| 4 |
+
from plugins.bing_connector import BingConnector
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class BingPlugin:
|
| 8 |
+
"""
|
| 9 |
+
A plugin to search Bing.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
def __init__(self, bing_api_key: str):
|
| 13 |
+
self.bing = BingConnector(api_key=bing_api_key)
|
| 14 |
+
if not bing_api_key or bing_api_key == "...":
|
| 15 |
+
raise Exception("Bing API key is not set")
|
| 16 |
+
|
| 17 |
+
@sk_function(
|
| 18 |
+
description="Use Bing to find a page about a topic. The return is a URL of the page found.",
|
| 19 |
+
name="find_web_page_about",
|
| 20 |
+
input_description="Two comma separated values: #1 Offset from the first result (default zero), #2 The topic to search, e.g. '0,who won the F1 title in 2023?'.",
|
| 21 |
+
)
|
| 22 |
+
async def find_web_page_about(self, input: str) -> str:
|
| 23 |
+
"""
|
| 24 |
+
A native function that uses Bing to find a page URL about a topic.
|
| 25 |
+
To simplify the integration with Autogen, the input parameter is a string with two comma separated
|
| 26 |
+
values, rather than the usual context dictionary.
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
# Input validation, the error message can help self-correct the input
|
| 30 |
+
if "," not in input:
|
| 31 |
+
raise ValueError("The input argument must contain a comma, e.g. '0,who won the F1 title in 2023?'")
|
| 32 |
+
|
| 33 |
+
parts = input.split(",", 1)
|
| 34 |
+
result = await self.bing.search_url_async(query=parts[1], num_results=1, offset=parts[0])
|
| 35 |
+
if result:
|
| 36 |
+
return result[0]
|
| 37 |
+
else:
|
| 38 |
+
return f"Nothing found, try again or try to adjust the topic."
|
plugins/sk_web_pages_plugin.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Microsoft. All rights reserved.
|
| 2 |
+
|
| 3 |
+
from semantic_kernel.skill_definition import sk_function
|
| 4 |
+
from bs4 import BeautifulSoup
|
| 5 |
+
import re, aiohttp
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class WebPagesPlugin:
|
| 9 |
+
"""
|
| 10 |
+
A plugin to interact with web pages, e.g. download the text content of a page.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
@sk_function(
|
| 14 |
+
description="Fetch the text content of a webpage. The return is a string containing all the text.",
|
| 15 |
+
name="fetch_webpage",
|
| 16 |
+
input_description="URL of the page to fetch.",
|
| 17 |
+
)
|
| 18 |
+
async def fetch_webpage(self, input: str) -> str:
|
| 19 |
+
"""
|
| 20 |
+
A native function that fetches the text content of a webpage.
|
| 21 |
+
HTML tags are removed, and empty lines are compacted.
|
| 22 |
+
"""
|
| 23 |
+
if not input:
|
| 24 |
+
raise ValueError("url cannot be `None` or empty")
|
| 25 |
+
async with aiohttp.ClientSession() as session:
|
| 26 |
+
async with session.get(input, raise_for_status=True) as response:
|
| 27 |
+
html = await response.text()
|
| 28 |
+
soup = BeautifulSoup(html, features="html.parser")
|
| 29 |
+
# remove some elements
|
| 30 |
+
for el in soup(["script", "style", "iframe", "img", "video", "audio"]):
|
| 31 |
+
el.extract()
|
| 32 |
+
|
| 33 |
+
# get text and compact empty lines
|
| 34 |
+
text = soup.get_text()
|
| 35 |
+
return re.sub(r"[\r\n][\r\n]{2,}", "\n\n", text)
|