-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/first implementation #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
4658947
79a332e
392e473
ac7db18
22c1d86
480270f
b9b3384
3a784f3
66f5739
010dfc9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| * @edgee-cloud/edgeers |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| name: Check | ||
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
| pull_request: | ||
|
|
||
| jobs: | ||
| Check: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: "3.12" | ||
| # - name: Install | ||
| # run: pip install -r requirements.txt | ||
| # - name: Lint | ||
| # run: pylint edgee | ||
| # - name: Test | ||
| # run: python -m unittest discover -s tests -p 'test_*.py' | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| name: Release to PyPI | ||
|
|
||
| on: | ||
| push: | ||
| tags: | ||
| - "v*" | ||
|
|
||
| jobs: | ||
| release: | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| id-token: write # Required for trusted publishing | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Set up Python | ||
| uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: "3.12" | ||
|
|
||
| - name: Install build dependencies | ||
| run: pip install build | ||
|
|
||
| - name: Build package | ||
| run: python -m build | ||
|
|
||
| - name: Publish to PyPI | ||
| uses: pypa/gh-action-pypi-publish@release/v1 | ||
|
|
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,157 @@ | ||||||||||
| """Edgee Gateway SDK for Python""" | ||||||||||
|
|
||||||||||
| import os | ||||||||||
| import json | ||||||||||
| from typing import Optional, Union | ||||||||||
| from dataclasses import dataclass | ||||||||||
| from urllib.request import Request, urlopen | ||||||||||
| from urllib.error import HTTPError | ||||||||||
|
|
||||||||||
|
|
||||||||||
| @dataclass | ||||||||||
| class FunctionDefinition: | ||||||||||
| name: str | ||||||||||
| description: Optional[str] = None | ||||||||||
| parameters: Optional[dict] = None | ||||||||||
|
|
||||||||||
|
|
||||||||||
| @dataclass | ||||||||||
| class Tool: | ||||||||||
| type: str # "function" | ||||||||||
| function: FunctionDefinition | ||||||||||
|
|
||||||||||
|
|
||||||||||
| @dataclass | ||||||||||
| class ToolCall: | ||||||||||
| id: str | ||||||||||
| type: str | ||||||||||
| function: dict # {"name": str, "arguments": str} | ||||||||||
|
|
||||||||||
|
|
||||||||||
| @dataclass | ||||||||||
| class Message: | ||||||||||
| role: str # "system" | "user" | "assistant" | "tool" | ||||||||||
| content: Optional[str] = None | ||||||||||
| name: Optional[str] = None | ||||||||||
| tool_calls: Optional[list[ToolCall]] = None | ||||||||||
| tool_call_id: Optional[str] = None | ||||||||||
|
|
||||||||||
|
|
||||||||||
| @dataclass | ||||||||||
| class InputObject: | ||||||||||
| messages: list[dict] | ||||||||||
| tools: Optional[list[dict]] = None | ||||||||||
| tool_choice: Optional[Union[str, dict]] = None | ||||||||||
|
|
||||||||||
|
|
||||||||||
| @dataclass | ||||||||||
| class Choice: | ||||||||||
| index: int | ||||||||||
| message: dict | ||||||||||
| finish_reason: Optional[str] | ||||||||||
|
|
||||||||||
|
|
||||||||||
| @dataclass | ||||||||||
| class Usage: | ||||||||||
| prompt_tokens: int | ||||||||||
| completion_tokens: int | ||||||||||
| total_tokens: int | ||||||||||
|
|
||||||||||
|
|
||||||||||
| @dataclass | ||||||||||
| class SendResponse: | ||||||||||
| choices: list[Choice] | ||||||||||
| usage: Optional[Usage] = None | ||||||||||
|
|
||||||||||
|
|
||||||||||
| @dataclass | ||||||||||
| class EdgeeConfig: | ||||||||||
| api_key: Optional[str] = None | ||||||||||
| base_url: Optional[str] = None | ||||||||||
|
|
||||||||||
|
|
||||||||||
| class Edgee: | ||||||||||
| def __init__( | ||||||||||
| self, | ||||||||||
| config: Optional[Union[str, EdgeeConfig, dict]] = None, | ||||||||||
| ): | ||||||||||
| api_key: Optional[str] = None | ||||||||||
| base_url: Optional[str] = None | ||||||||||
|
|
||||||||||
| if isinstance(config, str): | ||||||||||
| # Backward compatibility: accept api_key as string | ||||||||||
| api_key = config | ||||||||||
| elif isinstance(config, EdgeeConfig): | ||||||||||
| api_key = config.api_key | ||||||||||
| base_url = config.base_url | ||||||||||
| elif isinstance(config, dict): | ||||||||||
| api_key = config.get("api_key") | ||||||||||
| base_url = config.get("base_url") | ||||||||||
|
|
||||||||||
| self.api_key = api_key or os.environ.get("EDGEE_API_KEY", "") | ||||||||||
| if not self.api_key: | ||||||||||
| raise ValueError("EDGEE_API_KEY is not set") | ||||||||||
|
|
||||||||||
| self.base_url = base_url or os.environ.get("EDGEE_BASE_URL", "https://api.edgee.ai") | ||||||||||
|
|
||||||||||
| def send( | ||||||||||
| self, | ||||||||||
| model: str, | ||||||||||
| input: Union[str, InputObject, dict], | ||||||||||
| ) -> SendResponse: | ||||||||||
| """Send a completion request to the Edgee AI Gateway.""" | ||||||||||
|
|
||||||||||
| if isinstance(input, str): | ||||||||||
| messages = [{"role": "user", "content": input}] | ||||||||||
| tools = None | ||||||||||
| tool_choice = None | ||||||||||
| elif isinstance(input, InputObject): | ||||||||||
| messages = input.messages | ||||||||||
| tools = input.tools | ||||||||||
| tool_choice = input.tool_choice | ||||||||||
| else: | ||||||||||
| messages = input.get("messages", []) | ||||||||||
| tools = input.get("tools") | ||||||||||
| tool_choice = input.get("tool_choice") | ||||||||||
|
|
||||||||||
| body: dict = {"model": model, "messages": messages} | ||||||||||
| if tools: | ||||||||||
| body["tools"] = tools | ||||||||||
| if tool_choice: | ||||||||||
| body["tool_choice"] = tool_choice | ||||||||||
|
|
||||||||||
| request = Request( | ||||||||||
| f"{self.base_url}/v1/chat/completions", | ||||||||||
| data=json.dumps(body).encode("utf-8"), | ||||||||||
| headers={ | ||||||||||
| "Content-Type": "application/json", | ||||||||||
| "Authorization": f"Bearer {self.api_key}", | ||||||||||
|
CLEMENTINATOR marked this conversation as resolved.
|
||||||||||
| }, | ||||||||||
| method="POST", | ||||||||||
| ) | ||||||||||
|
|
||||||||||
| try: | ||||||||||
| with urlopen(request) as response: | ||||||||||
|
Comment on lines
+225
to
+226
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reliability: Missing HTTP Timeout
The urlopen call lacks a timeout, which could cause the request to hang indefinitely if the server doesn't respond. Adding a reasonable timeout improves reliability. Code suggestionCheck the AI-generated fix before applying
Suggested change
Code Review Run #8e8543 Should Bito avoid suggestions like this for future reviews? (Manage Rules)
|
||||||||||
| data = json.loads(response.read().decode("utf-8")) | ||||||||||
| except HTTPError as e: | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Error Handling: Incomplete Exception Coverage
Currently, only HTTPError is caught, but network connection failures raise URLError. Catching both ensures all request failures are handled gracefully. Code suggestionCheck the AI-generated fix before applying Code Review Run #8e8543 Should Bito avoid suggestions like this for future reviews? (Manage Rules)
|
||||||||||
| error_body = e.read().decode("utf-8") | ||||||||||
| raise RuntimeError(f"API error {e.code}: {error_body}") from e | ||||||||||
|
|
||||||||||
| choices = [ | ||||||||||
| Choice( | ||||||||||
| index=c["index"], | ||||||||||
| message=c["message"], | ||||||||||
| finish_reason=c.get("finish_reason"), | ||||||||||
| ) | ||||||||||
| for c in data["choices"] | ||||||||||
| ] | ||||||||||
|
|
||||||||||
| usage = None | ||||||||||
| if "usage" in data: | ||||||||||
| usage = Usage( | ||||||||||
| prompt_tokens=data["usage"]["prompt_tokens"], | ||||||||||
| completion_tokens=data["usage"]["completion_tokens"], | ||||||||||
| total_tokens=data["usage"]["total_tokens"], | ||||||||||
| ) | ||||||||||
|
|
||||||||||
| return SendResponse(choices=choices, usage=usage) | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| """Example usage of Edgee Gateway SDK""" | ||
|
|
||
| import os | ||
| import sys | ||
|
|
||
| # Add parent directory to path for local testing | ||
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | ||
|
|
||
| from edgee import Edgee | ||
|
|
||
| edgee = Edgee(os.environ.get("EDGEE_API_KEY", "test-key")) | ||
|
|
||
| # Test 1: Simple string input | ||
| print("Test 1: Simple string input") | ||
| response1 = edgee.send( | ||
| model="gpt-4o", | ||
| input="What is the capital of France?", | ||
| ) | ||
| print(f"Content: {response1.choices[0].message['content']}") | ||
| print(f"Usage: {response1.usage}") | ||
| print() | ||
|
|
||
| # Test 2: Full input object with messages | ||
| print("Test 2: Full input object with messages") | ||
| response2 = edgee.send( | ||
| model="gpt-4o", | ||
| input={ | ||
| "messages": [ | ||
| {"role": "system", "content": "You are a helpful assistant."}, | ||
| {"role": "user", "content": "Say hello!"}, | ||
| ], | ||
| }, | ||
| ) | ||
| print(f"Content: {response2.choices[0].message['content']}") | ||
| print() | ||
|
|
||
| # Test 3: With tools | ||
| print("Test 3: With tools") | ||
| response3 = edgee.send( | ||
| model="gpt-4o", | ||
| input={ | ||
| "messages": [{"role": "user", "content": "What is the weather in Paris?"}], | ||
| "tools": [ | ||
| { | ||
| "type": "function", | ||
| "function": { | ||
| "name": "get_weather", | ||
| "description": "Get the current weather for a location", | ||
| "parameters": { | ||
| "type": "object", | ||
| "properties": { | ||
| "location": {"type": "string", "description": "City name"}, | ||
| }, | ||
| "required": ["location"], | ||
| }, | ||
| }, | ||
| }, | ||
| ], | ||
| "tool_choice": "auto", | ||
| }, | ||
| ) | ||
| print(f"Content: {response3.choices[0].message.get('content')}") | ||
| print(f"Tool calls: {response3.choices[0].message.get('tool_calls')}") | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| [project] | ||
| name = "edgee" | ||
| version = "0.1.1" | ||
| description = "Lightweight Python SDK for Edgee AI Gateway" | ||
| readme = "README.md" | ||
| license = "Apache-2.0" | ||
| requires-python = ">=3.10" | ||
| dependencies = [] | ||
| authors = [ | ||
| { name = "Edgee", email = "support@edgee.cloud" } | ||
| ] | ||
| keywords = ["ai", "llm", "gateway", "openai", "anthropic"] | ||
| classifiers = [ | ||
| "Development Status :: 4 - Beta", | ||
| "Intended Audience :: Developers", | ||
| "License :: OSI Approved :: Apache Software License", | ||
| "Programming Language :: Python :: 3", | ||
| "Programming Language :: Python :: 3.10", | ||
| "Programming Language :: Python :: 3.11", | ||
| "Programming Language :: Python :: 3.12", | ||
| "Topic :: Software Development :: Libraries :: Python Modules", | ||
| ] | ||
|
|
||
| [project.urls] | ||
| Homepage = "https://github.com/edgee-cloud/python-sdk" | ||
| Repository = "https://github.com/edgee-cloud/python-sdk" | ||
|
|
||
| [project.optional-dependencies] | ||
| dev = ["pytest>=8.0.0"] | ||
|
|
||
| [build-system] | ||
| requires = ["hatchling"] | ||
| build-backend = "hatchling.build" | ||
|
|
||
| [tool.pytest.ini_options] | ||
| testpaths = ["tests"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add ruff check , ruff fmt