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)
|