You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

bing_search.py 8.7 kB

Enable LLM Call Observability in AGS (#5457) <!-- Thank you for your contribution! Please review https://microsoft.github.io/autogen/docs/Contribute before opening a pull request. --> <!-- Please add a reviewer to the assignee section when you create a PR. If you don't have the access to it, we will shortly find a reviewer and assign them to your PR. --> It is often helpful to inspect the raw request and response to/from an LLM as agents act. This PR, does the following: - Updates TeamManager to yield LLMCallEvents from core library - Run in an async background task to listen for LLMCallEvent JIT style when a team is run - Add events to an async queue and - yield those events in addition to whatever actual agentchat team.run_stream yields. - Update the AGS UI to show those LLMCallEvents in the messages section as a team runs - Add settings panel to show/hide llm call events in messages. - Minor updates to default team <img width="1539" alt="image" src="https://github.com/user-attachments/assets/bfbb19fe-3560-4faa-b600-7dd244e0e974" /> <img width="1554" alt="image" src="https://github.com/user-attachments/assets/775624f5-ba83-46e8-81ff-512bfeed6bab" /> <img width="1538" alt="image" src="https://github.com/user-attachments/assets/3becbf85-b75a-4506-adb7-e5575ebbaec4" /> ## Why are these changes needed? <!-- Please give a short summary of the change and the problem this solves. --> ## Related issue number <!-- For example: "Closes #1234" --> Closes #5440 ## Checks - [ ] I've included any doc changes needed for https://microsoft.github.io/autogen/. See https://microsoft.github.io/autogen/docs/Contribute#documentation to build and test documentation locally. - [ ] I've added tests (if relevant) corresponding to the changes introduced in this PR. - [ ] I've made sure all auto checks have passed.
1 year ago
Enable LLM Call Observability in AGS (#5457) <!-- Thank you for your contribution! Please review https://microsoft.github.io/autogen/docs/Contribute before opening a pull request. --> <!-- Please add a reviewer to the assignee section when you create a PR. If you don't have the access to it, we will shortly find a reviewer and assign them to your PR. --> It is often helpful to inspect the raw request and response to/from an LLM as agents act. This PR, does the following: - Updates TeamManager to yield LLMCallEvents from core library - Run in an async background task to listen for LLMCallEvent JIT style when a team is run - Add events to an async queue and - yield those events in addition to whatever actual agentchat team.run_stream yields. - Update the AGS UI to show those LLMCallEvents in the messages section as a team runs - Add settings panel to show/hide llm call events in messages. - Minor updates to default team <img width="1539" alt="image" src="https://github.com/user-attachments/assets/bfbb19fe-3560-4faa-b600-7dd244e0e974" /> <img width="1554" alt="image" src="https://github.com/user-attachments/assets/775624f5-ba83-46e8-81ff-512bfeed6bab" /> <img width="1538" alt="image" src="https://github.com/user-attachments/assets/3becbf85-b75a-4506-adb7-e5575ebbaec4" /> ## Why are these changes needed? <!-- Please give a short summary of the change and the problem this solves. --> ## Related issue number <!-- For example: "Closes #1234" --> Closes #5440 ## Checks - [ ] I've included any doc changes needed for https://microsoft.github.io/autogen/. See https://microsoft.github.io/autogen/docs/Contribute#documentation to build and test documentation locally. - [ ] I've added tests (if relevant) corresponding to the changes introduced in this PR. - [ ] I've made sure all auto checks have passed.
1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. import json
  2. import os
  3. from typing import Dict, List, Optional
  4. from urllib.parse import urljoin
  5. import html2text
  6. import httpx
  7. from autogen_core.code_executor import ImportFromModule
  8. from autogen_core.tools import FunctionTool
  9. from bs4 import BeautifulSoup
  10. async def bing_search(
  11. query: str,
  12. num_results: int = 3,
  13. include_snippets: bool = True,
  14. include_content: bool = True,
  15. content_max_length: Optional[int] = 10000,
  16. language: str = "en",
  17. country: Optional[str] = None,
  18. safe_search: str = "moderate",
  19. response_filter: str = "webpages",
  20. ) -> List[Dict[str, str]]:
  21. """
  22. Perform a Bing search using the Bing Web Search API.
  23. Args:
  24. query: Search query string
  25. num_results: Number of results to return (max 50)
  26. include_snippets: Include result snippets in output
  27. include_content: Include full webpage content in markdown format
  28. content_max_length: Maximum length of webpage content (if included)
  29. language: Language code for search results (e.g., 'en', 'es', 'fr')
  30. country: Optional market code for search results (e.g., 'us', 'uk')
  31. safe_search: SafeSearch setting ('off', 'moderate', or 'strict')
  32. response_filter: Type of results ('webpages', 'news', 'images', or 'videos')
  33. Returns:
  34. List[Dict[str, str]]: List of search results
  35. Raises:
  36. ValueError: If API credentials are invalid or request fails
  37. """
  38. # Get and validate API key
  39. api_key = os.getenv("BING_SEARCH_KEY", "").strip()
  40. if not api_key:
  41. raise ValueError(
  42. "BING_SEARCH_KEY environment variable is not set. " "Please obtain an API key from Azure Portal."
  43. )
  44. # Validate safe_search parameter
  45. valid_safe_search = ["off", "moderate", "strict"]
  46. if safe_search.lower() not in valid_safe_search:
  47. raise ValueError(f"Invalid safe_search value. Must be one of: {', '.join(valid_safe_search)}")
  48. # Validate response_filter parameter
  49. valid_filters = ["webpages", "news", "images", "videos"]
  50. if response_filter.lower() not in valid_filters:
  51. raise ValueError(f"Invalid response_filter value. Must be one of: {', '.join(valid_filters)}")
  52. async def fetch_page_content(url: str, max_length: Optional[int] = 50000) -> str:
  53. """Helper function to fetch and convert webpage content to markdown"""
  54. headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}
  55. try:
  56. async with httpx.AsyncClient() as client:
  57. response = await client.get(url, headers=headers, timeout=10)
  58. response.raise_for_status()
  59. soup = BeautifulSoup(response.text, "html.parser")
  60. # Remove script and style elements
  61. for script in soup(["script", "style"]):
  62. script.decompose()
  63. # Convert relative URLs to absolute
  64. for tag in soup.find_all(["a", "img"]):
  65. if tag.get("href"):
  66. tag["href"] = urljoin(url, tag["href"])
  67. if tag.get("src"):
  68. tag["src"] = urljoin(url, tag["src"])
  69. h2t = html2text.HTML2Text()
  70. h2t.body_width = 0
  71. h2t.ignore_images = False
  72. h2t.ignore_emphasis = False
  73. h2t.ignore_links = False
  74. h2t.ignore_tables = False
  75. markdown = h2t.handle(str(soup))
  76. if max_length and len(markdown) > max_length:
  77. markdown = markdown[:max_length] + "\n...(truncated)"
  78. return markdown.strip()
  79. except Exception as e:
  80. return f"Error fetching content: {str(e)}"
  81. # Build request headers and parameters
  82. headers = {"Ocp-Apim-Subscription-Key": api_key, "Accept": "application/json"}
  83. params = {
  84. "q": query,
  85. "count": min(max(1, num_results), 50),
  86. "mkt": f"{language}-{country.upper()}" if country else language,
  87. "safeSearch": safe_search.capitalize(),
  88. "responseFilter": response_filter,
  89. "textFormat": "raw",
  90. }
  91. # Make the request
  92. try:
  93. async with httpx.AsyncClient() as client:
  94. response = await client.get(
  95. "https://api.bing.microsoft.com/v7.0/search", headers=headers, params=params, timeout=10
  96. )
  97. # Handle common error cases
  98. if response.status_code == 401:
  99. raise ValueError("Authentication failed. Please verify your Bing Search API key.")
  100. elif response.status_code == 403:
  101. raise ValueError(
  102. "Access forbidden. This could mean:\n"
  103. "1. The API key is invalid\n"
  104. "2. The API key has expired\n"
  105. "3. You've exceeded your API quota"
  106. )
  107. elif response.status_code == 429:
  108. raise ValueError("API quota exceeded. Please try again later.")
  109. response.raise_for_status()
  110. data = response.json()
  111. # Process results based on response_filter
  112. results = []
  113. if response_filter == "webpages" and "webPages" in data:
  114. items = data["webPages"]["value"]
  115. elif response_filter == "news" and "news" in data:
  116. items = data["news"]["value"]
  117. elif response_filter == "images" and "images" in data:
  118. items = data["images"]["value"]
  119. elif response_filter == "videos" and "videos" in data:
  120. items = data["videos"]["value"]
  121. else:
  122. if not any(key in data for key in ["webPages", "news", "images", "videos"]):
  123. return [] # No results found
  124. raise ValueError(f"No {response_filter} results found in API response")
  125. # Extract relevant information based on result type
  126. for item in items:
  127. result = {"title": item.get("name", "")}
  128. if response_filter == "webpages":
  129. result["link"] = item.get("url", "")
  130. if include_snippets:
  131. result["snippet"] = item.get("snippet", "")
  132. if include_content:
  133. result["content"] = await fetch_page_content(result["link"], max_length=content_max_length)
  134. elif response_filter == "news":
  135. result["link"] = item.get("url", "")
  136. if include_snippets:
  137. result["snippet"] = item.get("description", "")
  138. result["date"] = item.get("datePublished", "")
  139. if include_content:
  140. result["content"] = await fetch_page_content(result["link"], max_length=content_max_length)
  141. elif response_filter == "images":
  142. result["link"] = item.get("contentUrl", "")
  143. result["thumbnail"] = item.get("thumbnailUrl", "")
  144. if include_snippets:
  145. result["snippet"] = item.get("description", "")
  146. elif response_filter == "videos":
  147. result["link"] = item.get("contentUrl", "")
  148. result["thumbnail"] = item.get("thumbnailUrl", "")
  149. if include_snippets:
  150. result["snippet"] = item.get("description", "")
  151. result["duration"] = item.get("duration", "")
  152. results.append(result)
  153. return results[:num_results]
  154. except httpx.RequestException as e:
  155. error_msg = str(e)
  156. if "InvalidApiKey" in error_msg:
  157. raise ValueError("Invalid API key. Please check your BING_SEARCH_KEY environment variable.") from e
  158. elif "KeyExpired" in error_msg:
  159. raise ValueError("API key has expired. Please generate a new key.") from e
  160. else:
  161. raise ValueError(f"Search request failed: {error_msg}") from e
  162. except json.JSONDecodeError:
  163. raise ValueError("Failed to parse API response. " "Please verify your API credentials and try again.") from None
  164. except Exception as e:
  165. raise ValueError(f"Unexpected error during search: {str(e)}") from e
  166. # Create the Bing search tool
  167. bing_search_tool = FunctionTool(
  168. func=bing_search,
  169. description="""
  170. Perform Bing searches using the Bing Web Search API. Requires BING_SEARCH_KEY environment variable.
  171. Supports web, news, image, and video searches.
  172. See function documentation for detailed setup instructions.
  173. """,
  174. global_imports=[
  175. ImportFromModule("typing", ("List", "Dict", "Optional")),
  176. "os",
  177. "httpx",
  178. "json",
  179. "html2text",
  180. ImportFromModule("bs4", ("BeautifulSoup",)),
  181. ImportFromModule("urllib.parse", ("urljoin",)),
  182. ],
  183. )