|
| 1 | +"""Example code for planner-worker agent collaboration with multiple tools.""" |
| 2 | + |
| 3 | +import asyncio |
| 4 | +import contextlib |
| 5 | +import signal |
| 6 | +import sys |
| 7 | + |
| 8 | +import agents |
| 9 | +import gradio as gr |
| 10 | +from dotenv import load_dotenv |
| 11 | +from gradio.components.chatbot import ChatMessage |
| 12 | +from openai import AsyncOpenAI |
| 13 | + |
| 14 | +from src.utils import ( |
| 15 | + AsyncWeaviateKnowledgeBase, |
| 16 | + Configs, |
| 17 | + get_weaviate_async_client, |
| 18 | + oai_agent_stream_to_gradio_messages, |
| 19 | + set_up_logging, |
| 20 | + setup_langfuse_tracer, |
| 21 | +) |
| 22 | +from src.utils.langfuse.shared_client import langfuse_client |
| 23 | +from src.utils.tools.gemini_grounding import ( |
| 24 | + GeminiGroundingWithGoogleSearch, |
| 25 | + ModelSettings, |
| 26 | +) |
| 27 | + |
| 28 | + |
| 29 | +load_dotenv(verbose=True) |
| 30 | + |
| 31 | +set_up_logging() |
| 32 | + |
| 33 | +AGENT_LLM_NAMES = { |
| 34 | + "worker": "gemini-2.5-flash", # less expensive, |
| 35 | + "planner": "gemini-2.5-pro", # more expensive, better at reasoning and planning |
| 36 | +} |
| 37 | + |
| 38 | +configs = Configs.from_env_var() |
| 39 | +async_weaviate_client = get_weaviate_async_client( |
| 40 | + http_host=configs.weaviate_http_host, |
| 41 | + http_port=configs.weaviate_http_port, |
| 42 | + http_secure=configs.weaviate_http_secure, |
| 43 | + grpc_host=configs.weaviate_grpc_host, |
| 44 | + grpc_port=configs.weaviate_grpc_port, |
| 45 | + grpc_secure=configs.weaviate_grpc_secure, |
| 46 | + api_key=configs.weaviate_api_key, |
| 47 | +) |
| 48 | +async_openai_client = AsyncOpenAI() |
| 49 | +async_knowledgebase = AsyncWeaviateKnowledgeBase( |
| 50 | + async_weaviate_client, |
| 51 | + collection_name="enwiki_20250520", |
| 52 | +) |
| 53 | + |
| 54 | +gemini_grounding_tool = GeminiGroundingWithGoogleSearch( |
| 55 | + model_settings=ModelSettings(model=AGENT_LLM_NAMES["worker"]) |
| 56 | +) |
| 57 | + |
| 58 | + |
| 59 | +async def _cleanup_clients() -> None: |
| 60 | + """Close async clients.""" |
| 61 | + await async_weaviate_client.close() |
| 62 | + await async_openai_client.close() |
| 63 | + |
| 64 | + |
| 65 | +def _handle_sigint(signum: int, frame: object) -> None: |
| 66 | + """Handle SIGINT signal to gracefully shutdown.""" |
| 67 | + with contextlib.suppress(Exception): |
| 68 | + asyncio.get_event_loop().run_until_complete(_cleanup_clients()) |
| 69 | + sys.exit(0) |
| 70 | + |
| 71 | + |
| 72 | +# Worker Agent: handles long context efficiently |
| 73 | +kb_agent = agents.Agent( |
| 74 | + name="KnowledgeBaseAgent", |
| 75 | + instructions=""" |
| 76 | + You are an agent specialized in searching a knowledge base. |
| 77 | + You will receive a single search query as input. |
| 78 | + Use the 'search_knowledgebase' tool to perform a search, then return a |
| 79 | + JSON object with: |
| 80 | + - 'summary': a concise synthesis of the retrieved information in your own words |
| 81 | + - 'sources': a list of citations with {type: "kb", title: "...", section: "..."} |
| 82 | + - 'no_results': true/false |
| 83 | +
|
| 84 | + If the tool returns no matches, set "no_results": true and keep "sources" empty. |
| 85 | + Do NOT make up information. Do NOT return raw search results or long quotes. |
| 86 | + """, |
| 87 | + tools=[ |
| 88 | + agents.function_tool(async_knowledgebase.search_knowledgebase), |
| 89 | + ], |
| 90 | + # a faster, smaller model for quick searches |
| 91 | + model=agents.OpenAIChatCompletionsModel( |
| 92 | + model=AGENT_LLM_NAMES["worker"], openai_client=async_openai_client |
| 93 | + ), |
| 94 | +) |
| 95 | + |
| 96 | +# Main Agent: more expensive and slower, but better at complex planning |
| 97 | +main_agent = agents.Agent( |
| 98 | + name="MainAgent", |
| 99 | + instructions=""" |
| 100 | + You are a deep research agent and your goal is to conduct in-depth, multi-turn |
| 101 | + research by breaking down complex queries, using the provided tools, and |
| 102 | + synthesizing the information into a comprehensive report. |
| 103 | +
|
| 104 | + You have access to the following tools: |
| 105 | + 1. 'search_knowledgebase' - use this tool to search for information in a |
| 106 | + knowledge base. The knowledge base reflects a subset of Wikipedia as |
| 107 | + of May 2025. |
| 108 | + 2. 'get_web_search_grounded_response' - use this tool for current events, |
| 109 | + news, fact-checking or when the information in the knowledge base is |
| 110 | + not sufficient to answer the question. |
| 111 | +
|
| 112 | + Both tools will not return raw search results or the sources themselves. |
| 113 | + Instead, they will return a concise summary of the key findings, along |
| 114 | + with the sources used to generate the summary. |
| 115 | +
|
| 116 | + For best performance, divide complex queries into simpler sub-queries |
| 117 | + Before calling either tool, always explain your reasoning for doing so. |
| 118 | +
|
| 119 | + Note that the 'get_web_search_grounded_response' tool will expand the query |
| 120 | + into multiple search queries and execute them. It will also return the |
| 121 | + queries it executed. Do not repeat them. |
| 122 | +
|
| 123 | + **Routing Guidelines:** |
| 124 | + - When answering a question, you should first try to use the 'search_knowledgebase' |
| 125 | + tool, unless the question requires recent information after May 2025 or |
| 126 | + has explicit recency cues. |
| 127 | + - If either tool returns insufficient information for a given query, try |
| 128 | + reformulating or using the other tool. You can call either tool multiple |
| 129 | + times to get the information you need to answer the user's question. |
| 130 | +
|
| 131 | + **Guidelines for synthesis** |
| 132 | + - After collecting results, write the final answer from your own synthesis. |
| 133 | + - Add a "Sources" section listing unique sources, formatted as: |
| 134 | + [1] Publisher - URL |
| 135 | + [2] Wikipedia: <Page Title> (Section: <section>) |
| 136 | + Order by first mention in your text. Every factual sentence in your final |
| 137 | + response must map to at least one source. |
| 138 | + - If web and knowledge base disagree, surface the disagreement and prefer sources |
| 139 | + with newer publication dates. |
| 140 | + - Do not invent URLs or sources. |
| 141 | + - If both tools fail, say so and suggest 2–3 refined queries. |
| 142 | +
|
| 143 | + Be sure to mention the sources in your response, including the URL if available, |
| 144 | + and do not make up information. |
| 145 | + """, |
| 146 | + # Allow the planner agent to invoke the worker agent. |
| 147 | + # The long context provided to the worker agent is hidden from the main agent. |
| 148 | + tools=[ |
| 149 | + kb_agent.as_tool( |
| 150 | + tool_name="search_knowledgebase", |
| 151 | + tool_description=( |
| 152 | + "Search the knowledge base for a query and return a concise summary " |
| 153 | + "of the key findings, along with the sources used to generate " |
| 154 | + "the summary" |
| 155 | + ), |
| 156 | + ), |
| 157 | + agents.function_tool(gemini_grounding_tool.get_web_search_grounded_response), |
| 158 | + ], |
| 159 | + # a larger, more capable model for planning and reasoning over summaries |
| 160 | + model=agents.OpenAIChatCompletionsModel( |
| 161 | + model=AGENT_LLM_NAMES["planner"], openai_client=async_openai_client |
| 162 | + ), |
| 163 | +) |
| 164 | + |
| 165 | + |
| 166 | +async def _main(question: str, gr_messages: list[ChatMessage]): |
| 167 | + setup_langfuse_tracer() |
| 168 | + |
| 169 | + # Use the main agent as the entry point- not the worker agent. |
| 170 | + with langfuse_client.start_as_current_span(name="Agents-SDK-Trace") as span: |
| 171 | + span.update(input=question) |
| 172 | + |
| 173 | + result_stream = agents.Runner.run_streamed(main_agent, input=question) |
| 174 | + async for _item in result_stream.stream_events(): |
| 175 | + gr_messages += oai_agent_stream_to_gradio_messages(_item) |
| 176 | + if len(gr_messages) > 0: |
| 177 | + yield gr_messages |
| 178 | + |
| 179 | + span.update(output=result_stream.final_output) |
| 180 | + |
| 181 | + |
| 182 | +demo = gr.ChatInterface( |
| 183 | + _main, |
| 184 | + title="2.3 Multi-Agent with Multiple Search Tools", |
| 185 | + type="messages", |
| 186 | + examples=[ |
| 187 | + "At which university did the SVP Software Engineering" |
| 188 | + " at Apple (as of June 2025) earn their engineering degree?", |
| 189 | + "How does the annual growth in the 50th-percentile income " |
| 190 | + "in the US compare with that in Canada?", |
| 191 | + ], |
| 192 | +) |
| 193 | + |
| 194 | +if __name__ == "__main__": |
| 195 | + async_openai_client = AsyncOpenAI() |
| 196 | + |
| 197 | + signal.signal(signal.SIGINT, _handle_sigint) |
| 198 | + |
| 199 | + try: |
| 200 | + demo.launch(share=True) |
| 201 | + finally: |
| 202 | + asyncio.run(_cleanup_clients()) |
0 commit comments