Skip to content

Commit c5bbfdc

Browse files
Added Basic Function Calling Support
1 parent 6158415 commit c5bbfdc

File tree

6 files changed

+276
-27
lines changed

6 files changed

+276
-27
lines changed

CHANGELOG.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.1.2] - 2024-11-09
9+
### Added
10+
- Enhanced Function Calling support with improved tool calls handling
11+
- Added comprehensive test suite with example usage
12+
- Added proper error handling for API responses
13+
14+
### Changed
15+
- Improved URL handling in BaseAPIHandler
16+
- Enhanced ChatMessage class to handle tool calls and function calls
17+
- Updated type hints and documentation
18+
- Restructured code for better maintainability
19+
20+
### Fixed
21+
- URL joining issues in API requests
22+
- Stream handling improvements
23+
- Better error handling for API responses
24+
825
## [0.1.1] - 2024-11-08
926
### Added
1027
- Updated the README file for better visual understanding.
@@ -16,4 +33,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1633
### Added
1734
- Initial release
1835

19-
[0.1.0]: https://github.com/SreejanPersonal/openai-unofficial/releases/tag/v1.0.0
36+
[0.1.2]: https://github.com/SreejanPersonal/openai-unofficial/compare/v0.1.1...v0.1.2
37+
[0.1.1]: https://github.com/SreejanPersonal/openai-unofficial/compare/v0.1.0...v0.1.1
38+
[0.1.0]: https://github.com/SreejanPersonal/openai-unofficial/releases/tag/v0.1.0

README.md

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ An Free & Unlimited unofficial Python SDK for the OpenAI API, providing seamless
2121
- [Chat Completion with Audio Preview Model](#chat-completion-with-audio-preview-model)
2222
- [Image Generation](#image-generation)
2323
- [Audio Speech Recognition with Whisper Model](#audio-speech-recognition-with-whisper-model)
24+
- [Function Calling and Tool Usage](#function-calling-and-tool-usage)
25+
- [Basic Function Calling](#basic-function-calling)
2426
- [Contributing](#contributing)
2527
- [License](#license)
2628

@@ -45,7 +47,7 @@ An Free & Unlimited unofficial Python SDK for the OpenAI API, providing seamless
4547
Install the package via pip:
4648

4749
```bash
48-
pip install openai-unofficial
50+
pip install -U openai-unofficial
4951
```
5052

5153
---
@@ -203,6 +205,95 @@ with open("speech.mp3", "rb") as audio_file:
203205
print("Transcription:", transcription.text)
204206
```
205207

208+
### Function Calling and Tool Usage
209+
210+
The SDK supports OpenAI's function calling capabilities, allowing you to define and use tools/functions in your conversations. Here are examples of function calling & tool usage:
211+
212+
#### Basic Function Calling
213+
214+
> ⚠️ **Important Note**: In the current version (0.1.2), complex or multiple function calling is not yet fully supported. The SDK currently supports basic function calling capabilities. Support for multiple function calls and more complex tool usage patterns will be added in upcoming releases.
215+
216+
```python
217+
from openai_unofficial import OpenAIUnofficial
218+
import json
219+
220+
client = OpenAIUnofficial()
221+
222+
# Define your functions as tools
223+
tools = [
224+
{
225+
"type": "function",
226+
"function": {
227+
"name": "get_current_weather",
228+
"description": "Get the current weather in a given location",
229+
"parameters": {
230+
"type": "object",
231+
"properties": {
232+
"location": {
233+
"type": "string",
234+
"description": "The city and state, e.g., San Francisco, CA"
235+
},
236+
"unit": {
237+
"type": "string",
238+
"enum": ["celsius", "fahrenheit"],
239+
"description": "The temperature unit"
240+
}
241+
},
242+
"required": ["location"]
243+
}
244+
}
245+
}
246+
]
247+
248+
# Function to actually get weather data
249+
def get_current_weather(location: str, unit: str = "celsius") -> str:
250+
# This is a mock function - replace with actual weather API call
251+
return f"The current weather in {location} is 22°{unit[0].upper()}"
252+
253+
# Initial conversation message
254+
messages = [
255+
{"role": "user", "content": "What's the weather like in London?"}
256+
]
257+
258+
# First API call to get function calling response
259+
response = client.chat.completions.create(
260+
model="gpt-4o-mini-2024-07-18",
261+
messages=messages,
262+
tools=tools,
263+
tool_choice="auto"
264+
)
265+
266+
# Get the assistant's message
267+
assistant_message = response.choices[0].message
268+
messages.append(assistant_message.to_dict())
269+
270+
# Check if the model wants to call a function
271+
if assistant_message.tool_calls:
272+
# Process each tool call
273+
for tool_call in assistant_message.tool_calls:
274+
function_name = tool_call.function.name
275+
function_args = json.loads(tool_call.function.arguments)
276+
277+
# Call the function and get the result
278+
function_response = get_current_weather(**function_args)
279+
280+
# Append the function response to messages
281+
messages.append({
282+
"role": "tool",
283+
"tool_call_id": tool_call.id,
284+
"name": function_name,
285+
"content": function_response
286+
})
287+
288+
# Get the final response from the model
289+
final_response = client.chat.completions.create(
290+
model="gpt-4o-mini-2024-07-18",
291+
messages=messages
292+
)
293+
294+
print("Final Response:", final_response.choices[0].message.content)
295+
```
296+
206297
---
207298

208299
## Contributing

pyproject.toml

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,28 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "openai-unofficial"
7-
version = "0.1.1"
7+
version = "0.1.2"
88
authors = [
99
{ name="DevsDoCode", email="devsdocode@gmail.com" },
1010
]
11-
description = "Unofficial OpenAI API Python SDK"
11+
description = "Free & Unlimited Unofficial OpenAI API Python SDK - Supports all OpenAI Models & Endpoints"
1212
readme = "README.md"
1313
requires-python = ">=3.7"
14+
keywords = ["openai", "ai", "machine-learning", "chatgpt", "gpt", "dall-e", "text-to-speech", "api", "tts"]
15+
license = { text = "MIT" }
1416
classifiers = [
15-
"Programming Language :: Python :: 3",
17+
"Development Status :: 4 - Beta",
18+
"Intended Audience :: Developers",
1619
"License :: OSI Approved :: MIT License",
1720
"Operating System :: OS Independent",
21+
"Programming Language :: Python :: 3",
22+
"Programming Language :: Python :: 3.7",
23+
"Programming Language :: Python :: 3.8",
24+
"Programming Language :: Python :: 3.9",
25+
"Programming Language :: Python :: 3.10",
26+
"Programming Language :: Python :: 3.11",
27+
"Topic :: Software Development :: Libraries :: Python Modules",
28+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
1829
]
1930
dependencies = [
2031
"requests>=2.28.0",
@@ -23,9 +34,10 @@ dependencies = [
2334
[project.urls]
2435
"Homepage" = "https://github.com/SreejanPersonal/openai-unofficial"
2536
"Bug Tracker" = "https://github.com/SreejanPersonal/openai-unofficial/issues"
37+
"Youtube" = "https://www.youtube.com/@DevsDoCode"
38+
"Instragram" = "https://www.instagram.com/sree.shades_/"
39+
"LinkedIn" = "https://www.linkedin.com/in/developer-sreejan/"
40+
"Twitter" = "https://twitter.com/Anand_Sreejan"
2641

2742
[tool.hatch.build.targets.wheel]
28-
packages = ["src/openai_unofficial"]
29-
30-
# python -m build
31-
# python -m twine upload dist/*
43+
packages = ["src/openai_unofficial"]

src/openai_unofficial/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
from .main import OpenAIUnofficial
22

3-
__version__ = "0.1.1"
3+
__version__ = "0.1.2"
44
__all__ = ["OpenAIUnofficial"]

src/openai_unofficial/main.py

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import requests
22
import json
3-
from typing import Optional, List, Union, Dict, Any, Iterator, TypeVar, Generic
3+
from typing import Optional, List, Union, Dict, Any, Iterator, TypeVar
44
from abc import ABC, abstractmethod
55
from dataclasses import dataclass
66
from enum import Enum
77
import logging
88
from urllib.parse import urljoin
9-
import base64
109

1110
# Configure logging
1211
logging.basicConfig(level=logging.INFO)
@@ -49,7 +48,7 @@ def _create_session(self) -> requests.Session:
4948
return session
5049

5150
def _make_request(self, method: str, endpoint: str, **kwargs) -> requests.Response:
52-
url = urljoin(self.config.base_url, endpoint)
51+
url = urljoin(self.config.base_url + '/', endpoint)
5352
try:
5453
response = self.session.request(
5554
method=method,
@@ -70,25 +69,64 @@ class BaseModel(ABC):
7069
def to_dict(self) -> Dict[str, Any]:
7170
pass
7271

72+
class FunctionCall(BaseModel):
73+
def __init__(self, data: Dict[str, Any]):
74+
self.name = data.get('name')
75+
self.arguments = data.get('arguments')
76+
77+
def to_dict(self) -> Dict[str, Any]:
78+
return {
79+
'name': self.name,
80+
'arguments': self.arguments
81+
}
82+
83+
class ToolCall(BaseModel):
84+
def __init__(self, data: Dict[str, Any]):
85+
self.id = data.get('id')
86+
self.type = data.get('type')
87+
self.function = FunctionCall(data.get('function', {})) if data.get('function') else None
88+
89+
def to_dict(self) -> Dict[str, Any]:
90+
return {
91+
'id': self.id,
92+
'type': self.type,
93+
'function': self.function.to_dict() if self.function else None
94+
}
95+
7396
class ChatMessage(BaseModel):
7497
def __init__(self, data: Dict[str, Any]):
7598
self.role = data.get('role')
7699
self.content = data.get('content')
100+
self.function_call = FunctionCall(data.get('function_call', {})) if data.get('function_call') else None
101+
self.tool_calls = [ToolCall(tc) for tc in data.get('tool_calls', [])] if data.get('tool_calls') else []
77102
self.audio = data.get('audio')
78-
103+
# For messages of role 'tool', include 'tool_call_id' and 'name'
104+
self.tool_call_id = data.get('tool_call_id')
105+
self.name = data.get('name')
106+
79107
def to_dict(self) -> Dict[str, Any]:
80-
return {
81-
'role': self.role,
82-
'content': self.content,
83-
**({'audio': self.audio} if self.audio else {})
84-
}
108+
message_dict = {'role': self.role}
109+
if self.content is not None:
110+
message_dict['content'] = self.content
111+
if self.function_call is not None:
112+
message_dict['function_call'] = self.function_call.to_dict()
113+
if self.tool_calls:
114+
message_dict['tool_calls'] = [tool_call.to_dict() for tool_call in self.tool_calls]
115+
if self.audio is not None:
116+
message_dict['audio'] = self.audio
117+
if self.role == 'tool':
118+
if self.tool_call_id:
119+
message_dict['tool_call_id'] = self.tool_call_id
120+
if self.name:
121+
message_dict['name'] = self.name
122+
return message_dict
85123

86124
class ChatCompletionChoice(BaseModel):
87125
def __init__(self, data: Dict[str, Any]):
88126
self.index = data.get('index')
89127
self.message = ChatMessage(data.get('message', {}))
90128
self.finish_reason = data.get('finish_reason')
91-
129+
92130
def to_dict(self) -> Dict[str, Any]:
93131
return {
94132
'index': self.index,
@@ -104,7 +142,7 @@ def __init__(self, data: Dict[str, Any]):
104142
self.model = data.get('model')
105143
self.choices = [ChatCompletionChoice(choice) for choice in data.get('choices', [])]
106144
self.usage = data.get('usage')
107-
145+
108146
def to_dict(self) -> Dict[str, Any]:
109147
return {
110148
'id': self.id,
@@ -122,7 +160,7 @@ def __init__(self, data: Dict[str, Any]):
122160
self.created = data.get('created')
123161
self.model = data.get('model')
124162
self.choices = [ChatCompletionChunkChoice(choice) for choice in data.get('choices', [])]
125-
163+
126164
def to_dict(self) -> Dict[str, Any]:
127165
return {
128166
'id': self.id,
@@ -137,7 +175,7 @@ def __init__(self, data: Dict[str, Any]):
137175
self.index = data.get('index')
138176
self.delta = ChatMessage(data.get('delta', {}))
139177
self.finish_reason = data.get('finish_reason')
140-
178+
141179
def to_dict(self) -> Dict[str, Any]:
142180
return {
143181
'index': self.index,
@@ -149,7 +187,7 @@ class ImageGenerationResponse(BaseModel):
149187
def __init__(self, data: Dict[str, Any]):
150188
self.created = data.get('created')
151189
self.data = [ImageData(item) for item in data.get('data', [])]
152-
190+
153191
def to_dict(self) -> Dict[str, Any]:
154192
return {
155193
'created': self.created,
@@ -160,7 +198,7 @@ class ImageData(BaseModel):
160198
def __init__(self, data: Dict[str, Any]):
161199
self.url = data.get('url')
162200
self.b64_json = data.get('b64_json')
163-
201+
164202
def to_dict(self) -> Dict[str, Any]:
165203
return {
166204
'url': self.url,
@@ -173,7 +211,7 @@ def __init__(self, api_handler: BaseAPIHandler):
173211

174212
def create(
175213
self,
176-
messages: List[Dict[str, str]],
214+
messages: List[Dict[str, Any]],
177215
model: str = "gpt-4o-mini-2024-07-18",
178216
temperature: float = 0.7,
179217
top_p: float = 1.0,
@@ -182,6 +220,8 @@ def create(
182220
frequency_penalty: float = 0,
183221
modalities: List[str] = None,
184222
audio: Dict[str, str] = None,
223+
tools: List[Dict[str, Any]] = None,
224+
tool_choice: str = None,
185225
**kwargs
186226
) -> Union[ChatCompletionResponse, Iterator[ChatCompletionChunk]]:
187227
payload = {
@@ -199,6 +239,10 @@ def create(
199239
payload["modalities"] = modalities
200240
if audio:
201241
payload["audio"] = audio
242+
if tools:
243+
payload["tools"] = tools
244+
if tool_choice:
245+
payload["tool_choice"] = tool_choice
202246

203247
if stream:
204248
response = self.api_handler._make_request(
@@ -227,7 +271,7 @@ def _handle_streaming_response(self, response: requests.Response) -> Iterator[Ch
227271
line_str = line_str[len('data: '):]
228272
data = json.loads(line_str)
229273
yield ChatCompletionChunk(data)
230-
except:
274+
except Exception as e:
231275
continue
232276

233277
class Audio:

0 commit comments

Comments
 (0)