Skip to content

Commit 21f8ae2

Browse files
author
konrad-czarnota-ds
authored
feat: native openai tools support (#651)
1 parent ce5386b commit 21f8ae2

File tree

10 files changed

+347
-2
lines changed

10 files changed

+347
-2
lines changed

docs/how-to/agents/define_and_use_agents.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,35 @@ AgentResult(content='The current temperature in Tokyo is 10°C.', ...)
8080
For use cases where you want to process partial outputs from the LLM as they arrive (e.g., in chat UIs), the [`Agent`][ragbits.agents.Agent] class supports streaming through the `run_streaming()` method.
8181

8282
This method returns an `AgentResultStreaming` object — an async iterator that yields parts of the LLM response and tool-related events in real time.
83+
84+
## Native OpenAI tools
85+
Ragbits supports selected native OpenAI tools (web_search_preview, image_generation and code_interpreter). You can use them together with your tools.
86+
```python
87+
from ragbits.agents.tools import get_web_search_tool
88+
89+
async def main() -> None:
90+
"""Run the weather agent with additional tool."""
91+
model_name = "gpt-4o-2024-08-06"
92+
llm = LiteLLM(model_name=model_name, use_structured_output=True)
93+
agent = Agent(llm=llm, prompt=WeatherPrompt, tools=[get_web_search_tool(model_name)], keep_history=True)
94+
95+
response = await agent.run(WeatherPromptInput(location="Paris"))
96+
print(response)
97+
```
98+
99+
Tool descriptions are available [here](https://platform.openai.com/docs/guides/tools?api-mode=responses). For each of these you can see detailed
100+
information on the corresponding sub-pages (i.e. [here](https://platform.openai.com/docs/guides/tools-web-search?api-mode=responses#user-location) for web search).
101+
You can use default parameters or specify your own as a dict. For web search this might look like that:
102+
```python
103+
from ragbits.agents.tools import get_web_search_tool
104+
105+
tool_params = {
106+
"user_location": {
107+
"type": "approximate",
108+
"country": "GB",
109+
"city": "London",
110+
"region": "London",
111+
}
112+
}
113+
web_search_tool = get_web_search_tool("gpt-4o", tool_params)
114+
```

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ All necessary details are provided in the comments at the top of each script.
3636
| [Offline Chat Interface](/examples/chat/offline_chat.py) | [ragbits-chat](/packages/ragbits-chat) | Example of how to use the `ChatInterface` to create a simple chat application that works offline. |
3737
| [Recontextualize Last Message](/examples/chat/recontextualize_message.py) | [ragbits-chat](/packages/ragbits-chat) | Example of how to use the `StandaloneMessageCompressor` compressor to recontextualize the last message in a conversation history. |
3838
| [Agents Tool Use](/examples/agents/tool_use.py) | [ragbits-agents](/packages/ragbits-agents) | Example of how to use agent with tools. |
39+
| [Agents OpenAI Native Tool Use](/examples/agents/openai_native_tool_use.py) | [ragbits-agents](/packages/ragbits-agents) | Example of how to use agent with OpenAI native tools. |
3940
| [MCP Local](/examples/agents/mcp/local.py) | [ragbits-agents](/packages/ragbits-agents) | Example of how to use the `Agent` class to connect with a local MCP server. |
4041
| [MCP SSE](/examples/agents/mcp/sse.py) | [ragbits-agents](/packages/ragbits-agents) | Example of how to use the `Agent` class to connect with a remote MCP server via SSE. |
4142
| [MCP Streamable HTTP](/examples/agents/mcp/streamable_http.py) | [ragbits-agents](/packages/ragbits-agents) | Example of how to use the `Agent` class to connect with a remote MCP server via HTTP. |
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""
2+
Ragbits Agents Example: OpenAI native tool use
3+
4+
This example shows how to use agent with native OpenAI tools.
5+
We provide a single method as a tool to the agent and expect it to call it when answering query.
6+
7+
To run the script, execute the following command:
8+
9+
```bash
10+
uv run examples/agents/openai_native_tool_use.py
11+
```
12+
"""
13+
14+
# /// script
15+
# requires-python = ">=3.10"
16+
# dependencies = [
17+
# "ragbits-core",
18+
# "ragbits-agents[openai]",
19+
# ]
20+
# ///
21+
import asyncio
22+
23+
from pydantic import BaseModel
24+
25+
from ragbits.agents import Agent
26+
from ragbits.agents.tools import get_web_search_tool
27+
from ragbits.core.llms import LiteLLM
28+
from ragbits.core.prompt import Prompt
29+
30+
31+
class SearchPromptInput(BaseModel):
32+
"""
33+
Input format for the Search Prompt.
34+
"""
35+
36+
query: str
37+
38+
39+
class SearchPrompt(Prompt[SearchPromptInput]):
40+
"""
41+
Prompt that does a web search with a given query.
42+
"""
43+
44+
system_prompt = """
45+
You are a helpful assistant that responds to user questions.
46+
"""
47+
48+
user_prompt = """
49+
Search web for {{ query }}.
50+
"""
51+
52+
53+
async def main() -> None:
54+
"""
55+
Run the example.
56+
"""
57+
model_name = "gpt-4o-2024-08-06"
58+
llm = LiteLLM(model_name=model_name, use_structured_output=True)
59+
agent = Agent(llm=llm, prompt=SearchPrompt, tools=[get_web_search_tool(model_name)])
60+
response = await agent.run(SearchPromptInput(query="What date is today?"))
61+
print(response)
62+
63+
64+
if __name__ == "__main__":
65+
asyncio.run(main())

examples/agents/tool_use.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class WeatherPrompt(Prompt[WeatherPromptInput]):
6363
"""
6464

6565
system_prompt = """
66-
You are a helpful assisstant that responds to user questions about weather.
66+
You are a helpful assistant that responds to user questions about weather.
6767
"""
6868

6969
user_prompt = """

packages/ragbits-agents/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

5+
- Add native openai tools support (#621)
56
- add Context to Agents (#715)
67

78
## 1.1.0 (2025-07-09)

packages/ragbits-agents/pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ a2a = [
4242
mcp = [
4343
"mcp>=1.9.4,<2.0.0",
4444
]
45+
openai = [
46+
"openai>=1.91.0,<2.0.0",
47+
]
4548

4649
[project.urls]
4750
"Homepage" = "https://github.com/deepsense-ai/ragbits"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from ragbits.agents.tools.openai import get_code_interpreter_tool, get_image_generation_tool, get_web_search_tool
2+
3+
__all__ = ["get_code_interpreter_tool", "get_image_generation_tool", "get_web_search_tool"]
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import base64
2+
from collections.abc import Callable
3+
from typing import cast
4+
5+
from openai import AsyncOpenAI
6+
from openai.types.responses import Response
7+
from openai.types.responses.tool_param import CodeInterpreter, ToolParam
8+
9+
10+
def get_web_search_tool(model_name: str, additional_params: dict | None = None) -> Callable:
11+
"""
12+
Returns a native OpenAI web search tool as function
13+
14+
Args:
15+
model_name: The name of the model
16+
additional_params: The additional tool parameters
17+
18+
Returns:
19+
web search function
20+
"""
21+
params_to_pass = additional_params if additional_params else {}
22+
tool_object = OpenAITools(model_name, {"type": "web_search_preview", **params_to_pass})
23+
return tool_object.search_web
24+
25+
26+
def get_code_interpreter_tool(model_name: str, additional_params: dict | None = None) -> Callable:
27+
"""
28+
Returns a native OpenAI code interpreter tool as function
29+
30+
Args:
31+
model_name: The name of the model
32+
additional_params: The additional tool parameters
33+
34+
Returns:
35+
code interpreter function
36+
"""
37+
params_to_pass = additional_params if additional_params else {}
38+
tool_object = OpenAITools(model_name, cast(CodeInterpreter, {"type": "code_interpreter", **params_to_pass}))
39+
return tool_object.code_interpreter
40+
41+
42+
def get_image_generation_tool(model_name: str, additional_params: dict | None = None) -> Callable:
43+
"""
44+
Returns a native OpenAI image generation tool as function
45+
46+
Args:
47+
model_name: The name of the model
48+
additional_params: The additional tool parameters
49+
50+
Returns:
51+
image generation function
52+
"""
53+
params_to_pass = additional_params if additional_params else {}
54+
tool_object = OpenAITools(model_name, {"type": "image_generation", **params_to_pass})
55+
return tool_object.image_generation
56+
57+
58+
class OpenAIResponsesLLM:
59+
"""
60+
Class serving as a wrapper for tool calls to responses API of OpenAI
61+
"""
62+
63+
def __init__(self, model_name: str, tool_param: ToolParam):
64+
self._client = AsyncOpenAI()
65+
self._model_name = model_name
66+
self._tool_param = tool_param
67+
68+
async def use_tool(self, query: str) -> Response:
69+
"""
70+
Uses tool based on query and returns output.
71+
72+
Args:
73+
query: query for the tool
74+
75+
Returns:
76+
Output of the tool
77+
"""
78+
return await self._client.responses.create(
79+
model=self._model_name,
80+
tools=[self._tool_param],
81+
tool_choice="required",
82+
input=query,
83+
)
84+
85+
86+
class OpenAITools:
87+
"""
88+
Class wrapping tool calls to responses API of OpenAI
89+
"""
90+
91+
AVAILABLE_TOOLS = {"web_search_preview", "code_interpreter", "image_generation"}
92+
93+
def __init__(self, model_name: str, tool_param: ToolParam):
94+
self._responses_llm = OpenAIResponsesLLM(model_name, tool_param)
95+
96+
async def search_web(self, query: str) -> str:
97+
"""
98+
Searches web for a query
99+
100+
Args:
101+
query: The query to search
102+
103+
Returns:
104+
The web search result
105+
"""
106+
return (await self._responses_llm.use_tool(query)).output_text
107+
108+
async def code_interpreter(self, query: str) -> str:
109+
"""
110+
Performs actions in code interpreter based on query
111+
112+
Args:
113+
query: The query with instructions
114+
115+
Returns:
116+
Output of the interpreter
117+
"""
118+
return (await self._responses_llm.use_tool(query)).output_text
119+
120+
async def image_generation(self, query: str, save_path: str = "generated_image.png") -> str:
121+
"""
122+
Generate image based on query.
123+
124+
Args:
125+
query: The query with instructions
126+
save_path: The path to save the generated image
127+
128+
Returns:
129+
LLM text output
130+
"""
131+
response = await self._responses_llm.use_tool(query)
132+
image_data = next((output.result for output in response.output if output.type == "image_generation_call"), None)
133+
134+
if image_data:
135+
with open(save_path, "wb") as f:
136+
f.write(base64.b64decode(image_data))
137+
text_prefix = f"Image saved to {save_path}\n"
138+
else:
139+
text_prefix = "No generated image was returned\n"
140+
141+
return text_prefix + response.output_text
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import base64
2+
from unittest.mock import AsyncMock, MagicMock, patch
3+
4+
import pytest
5+
6+
from ragbits.agents.tools.openai import OpenAITools
7+
8+
9+
@pytest.fixture
10+
def mock_responses_llm() -> MagicMock:
11+
"""Fixture for mocking OpenAIResponsesLLM."""
12+
mock = MagicMock()
13+
mock.use_tool = AsyncMock()
14+
return mock
15+
16+
17+
@patch("ragbits.agents.tools.openai.AsyncOpenAI")
18+
async def test_search_web(mock_async_openai: MagicMock, mock_responses_llm: MagicMock) -> None:
19+
"""Test that search_web calls use_tool and returns output_text."""
20+
mock_response = MagicMock()
21+
mock_response.output_text = "Test output"
22+
mock_responses_llm.use_tool.return_value = mock_response
23+
tools = OpenAITools("test_model", tool_param=MagicMock())
24+
tools._responses_llm = mock_responses_llm
25+
26+
result = await tools.search_web("test query")
27+
28+
mock_responses_llm.use_tool.assert_called_once_with("test query")
29+
assert result == "Test output"
30+
31+
32+
@patch("ragbits.agents.tools.openai.AsyncOpenAI")
33+
async def test_code_interpreter(mock_async_openai: MagicMock, mock_responses_llm: MagicMock) -> None:
34+
"""Test that code_interpreter calls use_tool and returns output_text."""
35+
mock_response = MagicMock()
36+
mock_response.output_text = "Interpreter output"
37+
mock_responses_llm.use_tool.return_value = mock_response
38+
tools = OpenAITools("test_model", tool_param=MagicMock())
39+
tools._responses_llm = mock_responses_llm
40+
41+
result = await tools.code_interpreter("run code")
42+
43+
mock_responses_llm.use_tool.assert_called_once_with("run code")
44+
assert result == "Interpreter output"
45+
46+
47+
@patch("ragbits.agents.tools.openai.AsyncOpenAI")
48+
@patch("builtins.open", new_callable=MagicMock)
49+
async def test_image_generation(
50+
mock_open: MagicMock, mock_async_openai: MagicMock, mock_responses_llm: MagicMock
51+
) -> None:
52+
"""Test image_generation saves image and returns correct text."""
53+
image_data = base64.b64encode(b"test_image_content").decode("utf-8")
54+
55+
mock_output_item = MagicMock()
56+
mock_output_item.type = "image_generation_call"
57+
mock_output_item.result = image_data
58+
59+
mock_response = MagicMock()
60+
mock_response.output_text = "Generated image."
61+
mock_response.output = [mock_output_item]
62+
63+
mock_file_handle = MagicMock()
64+
mock_open.return_value.__enter__.return_value = mock_file_handle
65+
66+
mock_responses_llm.use_tool.return_value = mock_response
67+
tools = OpenAITools("test_model", tool_param=MagicMock())
68+
tools._responses_llm = mock_responses_llm
69+
70+
result = await tools.image_generation("a cat", save_path="cat.png")
71+
72+
mock_responses_llm.use_tool.assert_called_once_with("a cat")
73+
mock_open.assert_called_once_with("cat.png", "wb")
74+
mock_file_handle.write.assert_called_once_with(b"test_image_content")
75+
assert result == "Image saved to cat.png\nGenerated image."
76+
77+
78+
@patch("ragbits.agents.tools.openai.AsyncOpenAI")
79+
async def test_image_generation_no_image(mock_async_openai: MagicMock, mock_responses_llm: MagicMock) -> None:
80+
"""Test image_generation when no image is returned."""
81+
mock_output_item = MagicMock()
82+
mock_output_item.type = "image_generation_call"
83+
mock_output_item.result = None
84+
85+
mock_response = MagicMock()
86+
mock_response.output_text = "No image was generated."
87+
mock_response.output = [mock_output_item]
88+
89+
mock_responses_llm.use_tool.return_value = mock_response
90+
tools = OpenAITools("test_model", tool_param=MagicMock())
91+
tools._responses_llm = mock_responses_llm
92+
93+
result = await tools.image_generation("a dog")
94+
95+
assert result == "No generated image was returned\nNo image was generated."

0 commit comments

Comments
 (0)