11# Copyright (c) Microsoft. All rights reserved.
22
3- import asyncio
4- import concurrent .futures
53import json
6- from collections .abc import Awaitable , Callable , Collection , Set
7- from typing import Any , Literal
4+ from collections .abc import Collection , Set
5+ from typing import Literal
86
97import tiktoken
108
119from mcp_server_bing_search import settings
12- from mcp_server_bing_search .types import WebResult
10+ from mcp_server_bing_search .types import Link , WebResult
1311
1412
1513def lookup_url (url_hash : str ) -> str | None :
@@ -35,10 +33,31 @@ def lookup_url(url_hash: str) -> str | None:
3533 return None
3634
3735
36+ def consolidate_links (web_results : list [WebResult ]) -> list [Link ]:
37+ """
38+ Extracts and deduplicates links from a list of web results.
39+
40+ Args:
41+ web_results: List of WebResult objects containing links
42+
43+ Returns:
44+ A list of unique Link objects across all web results
45+ """
46+ unique_links = {}
47+
48+ for result in web_results :
49+ for link in result .links :
50+ if link .unique_id not in unique_links :
51+ unique_links [link .unique_id ] = link
52+
53+ return list (unique_links .values ())
54+
55+
3856def format_web_results (web_results : list [WebResult ]) -> str :
3957 """
4058 Creates an LLM friendly representation of the web results.
4159 """
60+ unique_links = consolidate_links (web_results )
4261
4362 formatted_results = ""
4463 for result in web_results :
@@ -47,14 +66,15 @@ def format_web_results(web_results: list[WebResult]) -> str:
4766 formatted_result += f"<url>{ result .url } </url>\n "
4867 formatted_result += f"<title>{ result .title } </title>\n "
4968 formatted_result += f"<content>{ result .content } \n </content>\n "
50- formatted_result += "<links>\n "
51- for link in result .links :
52- formatted_result += f"<link id={ link .unique_id } >{ link } </link>\n "
53-
54- formatted_result += "</links>\n "
5569 formatted_result += "</website>\n "
5670 formatted_results += formatted_result
5771
72+ if unique_links :
73+ formatted_results += "<links>\n "
74+ for link in unique_links :
75+ formatted_results += f"<link id={ link .unique_id } >{ link } </link>\n "
76+ formatted_results += "</links>\n "
77+
5878 return formatted_results .strip ()
5979
6080
@@ -64,7 +84,7 @@ def __init__(
6484 model : str ,
6585 allowed_special : Literal ["all" ] | Set [str ] | None = None ,
6686 disallowed_special : Literal ["all" ] | Collection [str ] | None = None ,
67- ):
87+ ) -> None :
6888 self .model = model
6989 self .allowed_special = allowed_special
7090 self .disallowed_special = disallowed_special
@@ -109,66 +129,3 @@ def num_tokens_in_str(self, text: str) -> int:
109129 disallowed_special = self .disallowed_special if self .disallowed_special is not None else (),
110130 )
111131 )
112-
113-
114- async def embarrassingly_parallel_async (
115- func : Callable [..., Awaitable [Any ]],
116- args_list : tuple [tuple [Any , ...], ...] | None = None ,
117- kwargs_list : list [dict [str , Any ]] | None = None ,
118- concurrency_limit : int = 10 ,
119- ) -> list [Any ]:
120- """Execute multiple async functions independently in separate threads.
121-
122- Each async function runs in its own thread with its own event loop,
123- providing true isolation between tasks.
124-
125- Args:
126- func: An async function that returns an awaitable
127- args_list: A tuple of tuples each containing positional arguments
128- kwargs_list: A list of dictionaries containing keyword arguments
129- concurrency_limit: Maximum number of threads to use (None means ThreadPoolExecutor default)
130-
131- Raises:
132- ValueError: If neither args_list nor kwargs_list is provided
133- ValueError: If both are provided but have different lengths
134-
135- Returns:
136- list[Any]: Results from each function call in the order tasks were submitted
137- """
138- if not args_list and not kwargs_list :
139- raise ValueError ("Either args_list or kwargs_list must be provided" )
140-
141- if args_list and kwargs_list and len (args_list ) != len (kwargs_list ):
142- raise ValueError ("args_list and kwargs_list must be of the same length" )
143-
144- def run_async_in_thread (idx : int ) -> Any :
145- """Run an async function in a new event loop in the current thread."""
146- args = args_list [idx ] if args_list else ()
147- kwargs = kwargs_list [idx ] if kwargs_list else {}
148-
149- # Create a new event loop for this thread
150- loop = asyncio .new_event_loop ()
151- asyncio .set_event_loop (loop )
152- try :
153- # Run the async function to completion and return the result
154- return loop .run_until_complete (func (* args , ** kwargs ))
155- finally :
156- # Clean up
157- loop .close ()
158-
159- task_count = len (args_list ) if args_list else len (kwargs_list or [])
160- results = []
161-
162- # Use ThreadPoolExecutor to run each task in a separate thread
163- with concurrent .futures .ThreadPoolExecutor (max_workers = concurrency_limit ) as executor :
164- # Submit all tasks and collect their futures
165- futures = [executor .submit (run_async_in_thread , i ) for i in range (task_count )]
166-
167- # Retrieve results in the order they were submitted
168- for future in concurrent .futures .as_completed (futures ):
169- try :
170- results .append (future .result ())
171- except Exception as exc :
172- results .append (exc )
173-
174- return results
0 commit comments