Tonic commited on
Commit
fa2eb35
1 Parent(s): 7edbd92

Upload 4 files

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