Skip to content

Commit

Permalink
Added Basic Function Calling Support
Browse files Browse the repository at this point in the history
  • Loading branch information
SreejanPersonal authored Nov 8, 2024
1 parent 6158415 commit c5bbfdc
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 27 deletions.
21 changes: 20 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.2] - 2024-11-09
### Added
- Enhanced Function Calling support with improved tool calls handling
- Added comprehensive test suite with example usage
- Added proper error handling for API responses

### Changed
- Improved URL handling in BaseAPIHandler
- Enhanced ChatMessage class to handle tool calls and function calls
- Updated type hints and documentation
- Restructured code for better maintainability

### Fixed
- URL joining issues in API requests
- Stream handling improvements
- Better error handling for API responses

## [0.1.1] - 2024-11-08
### Added
- Updated the README file for better visual understanding.
Expand All @@ -16,4 +33,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Initial release

[0.1.0]: https://github.com/SreejanPersonal/openai-unofficial/releases/tag/v1.0.0
[0.1.2]: https://github.com/SreejanPersonal/openai-unofficial/compare/v0.1.1...v0.1.2
[0.1.1]: https://github.com/SreejanPersonal/openai-unofficial/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/SreejanPersonal/openai-unofficial/releases/tag/v0.1.0
93 changes: 92 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ An Free & Unlimited unofficial Python SDK for the OpenAI API, providing seamless
- [Chat Completion with Audio Preview Model](#chat-completion-with-audio-preview-model)
- [Image Generation](#image-generation)
- [Audio Speech Recognition with Whisper Model](#audio-speech-recognition-with-whisper-model)
- [Function Calling and Tool Usage](#function-calling-and-tool-usage)
- [Basic Function Calling](#basic-function-calling)
- [Contributing](#contributing)
- [License](#license)

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

```bash
pip install openai-unofficial
pip install -U openai-unofficial
```

---
Expand Down Expand Up @@ -203,6 +205,95 @@ with open("speech.mp3", "rb") as audio_file:
print("Transcription:", transcription.text)
```

### Function Calling and Tool Usage

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:

#### Basic Function Calling

> ⚠️ **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.
```python
from openai_unofficial import OpenAIUnofficial
import json

client = OpenAIUnofficial()

# Define your functions as tools
tools = [
{
"type": "function",
"function": {
"name": "get_current_weather",
"description": "Get the current weather in a given location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g., San Francisco, CA"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "The temperature unit"
}
},
"required": ["location"]
}
}
}
]

# Function to actually get weather data
def get_current_weather(location: str, unit: str = "celsius") -> str:
# This is a mock function - replace with actual weather API call
return f"The current weather in {location} is 22°{unit[0].upper()}"

# Initial conversation message
messages = [
{"role": "user", "content": "What's the weather like in London?"}
]

# First API call to get function calling response
response = client.chat.completions.create(
model="gpt-4o-mini-2024-07-18",
messages=messages,
tools=tools,
tool_choice="auto"
)

# Get the assistant's message
assistant_message = response.choices[0].message
messages.append(assistant_message.to_dict())

# Check if the model wants to call a function
if assistant_message.tool_calls:
# Process each tool call
for tool_call in assistant_message.tool_calls:
function_name = tool_call.function.name
function_args = json.loads(tool_call.function.arguments)

# Call the function and get the result
function_response = get_current_weather(**function_args)

# Append the function response to messages
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"name": function_name,
"content": function_response
})

# Get the final response from the model
final_response = client.chat.completions.create(
model="gpt-4o-mini-2024-07-18",
messages=messages
)

print("Final Response:", final_response.choices[0].message.content)
```

---

## Contributing
Expand Down
26 changes: 19 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,28 @@ build-backend = "hatchling.build"

[project]
name = "openai-unofficial"
version = "0.1.1"
version = "0.1.2"
authors = [
{ name="DevsDoCode", email="[email protected]" },
]
description = "Unofficial OpenAI API Python SDK"
description = "Free & Unlimited Unofficial OpenAI API Python SDK - Supports all OpenAI Models & Endpoints"
readme = "README.md"
requires-python = ">=3.7"
keywords = ["openai", "ai", "machine-learning", "chatgpt", "gpt", "dall-e", "text-to-speech", "api", "tts"]
license = { text = "MIT" }
classifiers = [
"Programming Language :: Python :: 3",
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
]
dependencies = [
"requests>=2.28.0",
Expand All @@ -23,9 +34,10 @@ dependencies = [
[project.urls]
"Homepage" = "https://github.com/SreejanPersonal/openai-unofficial"
"Bug Tracker" = "https://github.com/SreejanPersonal/openai-unofficial/issues"
"Youtube" = "https://www.youtube.com/@DevsDoCode"
"Instragram" = "https://www.instagram.com/sree.shades_/"
"LinkedIn" = "https://www.linkedin.com/in/developer-sreejan/"
"Twitter" = "https://twitter.com/Anand_Sreejan"

[tool.hatch.build.targets.wheel]
packages = ["src/openai_unofficial"]

# python -m build
# python -m twine upload dist/*
packages = ["src/openai_unofficial"]
2 changes: 1 addition & 1 deletion src/openai_unofficial/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .main import OpenAIUnofficial

__version__ = "0.1.1"
__version__ = "0.1.2"
__all__ = ["OpenAIUnofficial"]
78 changes: 61 additions & 17 deletions src/openai_unofficial/main.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import requests
import json
from typing import Optional, List, Union, Dict, Any, Iterator, TypeVar, Generic
from typing import Optional, List, Union, Dict, Any, Iterator, TypeVar
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum
import logging
from urllib.parse import urljoin
import base64

# Configure logging
logging.basicConfig(level=logging.INFO)
Expand Down Expand Up @@ -49,7 +48,7 @@ def _create_session(self) -> requests.Session:
return session

def _make_request(self, method: str, endpoint: str, **kwargs) -> requests.Response:
url = urljoin(self.config.base_url, endpoint)
url = urljoin(self.config.base_url + '/', endpoint)
try:
response = self.session.request(
method=method,
Expand All @@ -70,25 +69,64 @@ class BaseModel(ABC):
def to_dict(self) -> Dict[str, Any]:
pass

class FunctionCall(BaseModel):
def __init__(self, data: Dict[str, Any]):
self.name = data.get('name')
self.arguments = data.get('arguments')

def to_dict(self) -> Dict[str, Any]:
return {
'name': self.name,
'arguments': self.arguments
}

class ToolCall(BaseModel):
def __init__(self, data: Dict[str, Any]):
self.id = data.get('id')
self.type = data.get('type')
self.function = FunctionCall(data.get('function', {})) if data.get('function') else None

def to_dict(self) -> Dict[str, Any]:
return {
'id': self.id,
'type': self.type,
'function': self.function.to_dict() if self.function else None
}

class ChatMessage(BaseModel):
def __init__(self, data: Dict[str, Any]):
self.role = data.get('role')
self.content = data.get('content')
self.function_call = FunctionCall(data.get('function_call', {})) if data.get('function_call') else None
self.tool_calls = [ToolCall(tc) for tc in data.get('tool_calls', [])] if data.get('tool_calls') else []
self.audio = data.get('audio')

# For messages of role 'tool', include 'tool_call_id' and 'name'
self.tool_call_id = data.get('tool_call_id')
self.name = data.get('name')

def to_dict(self) -> Dict[str, Any]:
return {
'role': self.role,
'content': self.content,
**({'audio': self.audio} if self.audio else {})
}
message_dict = {'role': self.role}
if self.content is not None:
message_dict['content'] = self.content
if self.function_call is not None:
message_dict['function_call'] = self.function_call.to_dict()
if self.tool_calls:
message_dict['tool_calls'] = [tool_call.to_dict() for tool_call in self.tool_calls]
if self.audio is not None:
message_dict['audio'] = self.audio
if self.role == 'tool':
if self.tool_call_id:
message_dict['tool_call_id'] = self.tool_call_id
if self.name:
message_dict['name'] = self.name
return message_dict

class ChatCompletionChoice(BaseModel):
def __init__(self, data: Dict[str, Any]):
self.index = data.get('index')
self.message = ChatMessage(data.get('message', {}))
self.finish_reason = data.get('finish_reason')

def to_dict(self) -> Dict[str, Any]:
return {
'index': self.index,
Expand All @@ -104,7 +142,7 @@ def __init__(self, data: Dict[str, Any]):
self.model = data.get('model')
self.choices = [ChatCompletionChoice(choice) for choice in data.get('choices', [])]
self.usage = data.get('usage')

def to_dict(self) -> Dict[str, Any]:
return {
'id': self.id,
Expand All @@ -122,7 +160,7 @@ def __init__(self, data: Dict[str, Any]):
self.created = data.get('created')
self.model = data.get('model')
self.choices = [ChatCompletionChunkChoice(choice) for choice in data.get('choices', [])]

def to_dict(self) -> Dict[str, Any]:
return {
'id': self.id,
Expand All @@ -137,7 +175,7 @@ def __init__(self, data: Dict[str, Any]):
self.index = data.get('index')
self.delta = ChatMessage(data.get('delta', {}))
self.finish_reason = data.get('finish_reason')

def to_dict(self) -> Dict[str, Any]:
return {
'index': self.index,
Expand All @@ -149,7 +187,7 @@ class ImageGenerationResponse(BaseModel):
def __init__(self, data: Dict[str, Any]):
self.created = data.get('created')
self.data = [ImageData(item) for item in data.get('data', [])]

def to_dict(self) -> Dict[str, Any]:
return {
'created': self.created,
Expand All @@ -160,7 +198,7 @@ class ImageData(BaseModel):
def __init__(self, data: Dict[str, Any]):
self.url = data.get('url')
self.b64_json = data.get('b64_json')

def to_dict(self) -> Dict[str, Any]:
return {
'url': self.url,
Expand All @@ -173,7 +211,7 @@ def __init__(self, api_handler: BaseAPIHandler):

def create(
self,
messages: List[Dict[str, str]],
messages: List[Dict[str, Any]],
model: str = "gpt-4o-mini-2024-07-18",
temperature: float = 0.7,
top_p: float = 1.0,
Expand All @@ -182,6 +220,8 @@ def create(
frequency_penalty: float = 0,
modalities: List[str] = None,
audio: Dict[str, str] = None,
tools: List[Dict[str, Any]] = None,
tool_choice: str = None,
**kwargs
) -> Union[ChatCompletionResponse, Iterator[ChatCompletionChunk]]:
payload = {
Expand All @@ -199,6 +239,10 @@ def create(
payload["modalities"] = modalities
if audio:
payload["audio"] = audio
if tools:
payload["tools"] = tools
if tool_choice:
payload["tool_choice"] = tool_choice

if stream:
response = self.api_handler._make_request(
Expand Down Expand Up @@ -227,7 +271,7 @@ def _handle_streaming_response(self, response: requests.Response) -> Iterator[Ch
line_str = line_str[len('data: '):]
data = json.loads(line_str)
yield ChatCompletionChunk(data)
except:
except Exception as e:
continue

class Audio:
Expand Down
Loading

0 comments on commit c5bbfdc

Please sign in to comment.