|
|
import gradio as gr |
|
|
import requests |
|
|
import json |
|
|
import base64 |
|
|
import os |
|
|
from typing import List, Optional, Tuple, Any |
|
|
import mimetypes |
|
|
from dotenv import load_dotenv |
|
|
|
|
|
|
|
|
load_dotenv() |
|
|
|
|
|
class OmniAPIClient: |
|
|
"""Client for interacting with the Omni API""" |
|
|
|
|
|
def __init__(self, base_url: str = "https://api.modelharbor.com"): |
|
|
self.base_url = base_url.rstrip('/') |
|
|
self.chat_endpoint = f"{self.base_url}/v1/chat/completions" |
|
|
self.models_endpoint = f"{self.base_url}/v1/models" |
|
|
|
|
|
def encode_file_to_base64(self, file_path: str) -> str: |
|
|
"""Encode file to base64 string""" |
|
|
with open(file_path, "rb") as file: |
|
|
return base64.b64encode(file.read()).decode('utf-8') |
|
|
|
|
|
def get_mime_type(self, file_path: str) -> str: |
|
|
"""Get MIME type of file""" |
|
|
mime_type, _ = mimetypes.guess_type(file_path) |
|
|
return mime_type or "application/octet-stream" |
|
|
|
|
|
def create_file_content(self, file_path: str, file_type: str) -> dict: |
|
|
"""Create file content object based on API format""" |
|
|
file_name = os.path.basename(file_path) |
|
|
mime_type = self.get_mime_type(file_path) |
|
|
|
|
|
|
|
|
if mime_type and mime_type.startswith('image/'): |
|
|
|
|
|
file_data_b64 = self.encode_file_to_base64(file_path) |
|
|
return { |
|
|
"type": "image_url", |
|
|
"image_url": { |
|
|
"url": f"data:{mime_type};base64,{file_data_b64}" |
|
|
} |
|
|
} |
|
|
else: |
|
|
|
|
|
file_data_b64 = self.encode_file_to_base64(file_path) |
|
|
return { |
|
|
"type": "file", |
|
|
"file": { |
|
|
"filename": file_name, |
|
|
"file_data": f"data:{mime_type};base64,{file_data_b64}" |
|
|
} |
|
|
} |
|
|
|
|
|
def build_message_content(self, text: str, files: List[str]) -> List[dict]: |
|
|
"""Build message content with text and files""" |
|
|
content_parts = [] |
|
|
|
|
|
|
|
|
if text.strip(): |
|
|
content_parts.append({ |
|
|
"type": "text", |
|
|
"text": text |
|
|
}) |
|
|
|
|
|
|
|
|
for file_path in files: |
|
|
if file_path and os.path.exists(file_path): |
|
|
file_content = self.create_file_content(file_path, "file") |
|
|
content_parts.append(file_content) |
|
|
|
|
|
return content_parts |
|
|
|
|
|
def get_available_models(self, api_key: str = "") -> Tuple[bool, List[str]]: |
|
|
"""Return fixed set of available models""" |
|
|
|
|
|
fixed_models = [ |
|
|
"typhoon-ocr-preview", |
|
|
"qwen/qwen3-vl-235b-a22b-instruct", |
|
|
"openai/gpt-5", |
|
|
"meta-llama/llama-4-maverick", |
|
|
"gemini/gemini-2.5-pro", |
|
|
"gemini/gemini-2.5-flash" |
|
|
] |
|
|
return True, fixed_models |
|
|
|
|
|
def send_chat_completion(self, text: str, files: List[str], api_key: str = "", model: str = "qwen/qwen3-vl-235b-a22b-instruct", max_tokens: int = 16384, stream: bool = False) -> Tuple[bool, Any]: |
|
|
"""Send chat completion request to the API""" |
|
|
try: |
|
|
|
|
|
content_parts = self.build_message_content(text, files) |
|
|
|
|
|
|
|
|
if not content_parts: |
|
|
return False, {"error": "No text or valid files provided"} |
|
|
|
|
|
|
|
|
payload = { |
|
|
"model": model, |
|
|
"messages": [ |
|
|
{ |
|
|
"role": "user", |
|
|
"content": content_parts |
|
|
} |
|
|
], |
|
|
"max_tokens": max_tokens, |
|
|
"stream": stream |
|
|
} |
|
|
|
|
|
|
|
|
headers = { |
|
|
"Content-Type": "application/json" |
|
|
} |
|
|
|
|
|
if api_key: |
|
|
headers["Authorization"] = f"Bearer {api_key}" |
|
|
|
|
|
|
|
|
response = requests.post( |
|
|
self.chat_endpoint, |
|
|
json=payload, |
|
|
headers=headers, |
|
|
timeout=600 |
|
|
) |
|
|
|
|
|
|
|
|
if response.status_code == 200: |
|
|
try: |
|
|
response_data = response.json() |
|
|
return True, response_data |
|
|
except json.JSONDecodeError: |
|
|
return False, {"error": "Invalid JSON response", "raw_response": response.text} |
|
|
else: |
|
|
try: |
|
|
error_data = response.json() |
|
|
return False, {"error": f"API Error ({response.status_code})", "details": error_data} |
|
|
except json.JSONDecodeError: |
|
|
return False, {"error": f"HTTP {response.status_code}", "raw_response": response.text} |
|
|
|
|
|
except requests.exceptions.Timeout: |
|
|
return False, {"error": "Request timeout"} |
|
|
except requests.exceptions.ConnectionError: |
|
|
return False, {"error": "Connection error"} |
|
|
except Exception as e: |
|
|
return False, {"error": f"Unexpected error: {str(e)}"} |
|
|
|
|
|
|
|
|
def create_ui(): |
|
|
"""Create the Gradio UI""" |
|
|
|
|
|
|
|
|
API_ENDPOINTS = { |
|
|
"https://api.modelharbor.com": [ |
|
|
"typhoon-ocr-preview", |
|
|
"qwen/qwen3-vl-235b-a22b-instruct", |
|
|
"openai/gpt-5", |
|
|
"meta-llama/llama-4-maverick", |
|
|
"gemini/gemini-2.5-pro", |
|
|
"gemini/gemini-2.5-flash" |
|
|
], |
|
|
"https://api-omni.modelharbor.com": [ |
|
|
"qwen/qwen3-235b-a22b-instruct-2507-omni" |
|
|
] |
|
|
} |
|
|
|
|
|
def fetch_models(api_endpoint): |
|
|
"""Return models based on selected API endpoint""" |
|
|
models = API_ENDPOINTS.get(api_endpoint, []) |
|
|
default_model = models[0] if models else "" |
|
|
return gr.Dropdown(choices=models, value=default_model) |
|
|
|
|
|
def send_request(api_endpoint, model, max_tokens, text, files): |
|
|
"""Handle request submission""" |
|
|
try: |
|
|
|
|
|
if not api_endpoint: |
|
|
return "β API endpoint is required", "" |
|
|
|
|
|
if not text.strip() and not files: |
|
|
return "β Please provide either text or upload files", "" |
|
|
|
|
|
|
|
|
api_key_to_use = os.getenv("API_KEY", "") |
|
|
|
|
|
|
|
|
client = OmniAPIClient(api_endpoint) |
|
|
|
|
|
|
|
|
valid_files = [] |
|
|
if files is not None: |
|
|
|
|
|
if isinstance(files, list): |
|
|
valid_files = [f.name for f in files if f is not None and hasattr(f, 'name')] |
|
|
elif hasattr(files, 'name'): |
|
|
|
|
|
valid_files = [files.name] |
|
|
|
|
|
|
|
|
success, response = client.send_chat_completion( |
|
|
text=text, |
|
|
files=valid_files, |
|
|
api_key=api_key_to_use, |
|
|
model=model, |
|
|
max_tokens=max_tokens |
|
|
) |
|
|
|
|
|
if success: |
|
|
|
|
|
formatted_response = json.dumps(response, indent=2) |
|
|
|
|
|
|
|
|
if "choices" in response and len(response["choices"]) > 0: |
|
|
choice = response["choices"][0] |
|
|
if "message" in choice and "content" in choice["message"]: |
|
|
|
|
|
if "typhoon" in model.lower(): |
|
|
try: |
|
|
|
|
|
json_content = json.loads(choice["message"]["content"]) |
|
|
if "natural_text" in json_content: |
|
|
assistant_reply = json_content["natural_text"] |
|
|
else: |
|
|
assistant_reply = choice["message"]["content"] |
|
|
except (KeyError, TypeError): |
|
|
|
|
|
assistant_reply = choice["message"]["content"] |
|
|
else: |
|
|
assistant_reply = choice["message"]["content"] |
|
|
|
|
|
status = f"β
Request successful\n\n**Assistant Reply:**\n{assistant_reply}" |
|
|
else: |
|
|
status = "β
Request successful" |
|
|
else: |
|
|
status = "β
Request successful" |
|
|
|
|
|
return status, formatted_response |
|
|
else: |
|
|
|
|
|
error_response = json.dumps(response, indent=2) |
|
|
return f"β Request failed", error_response |
|
|
|
|
|
except Exception as e: |
|
|
return f"β Error: {str(e)}", "" |
|
|
|
|
|
def clear_form(): |
|
|
"""Clear all form inputs""" |
|
|
return "", "", "", None |
|
|
|
|
|
|
|
|
css = """ |
|
|
.gradio-container { |
|
|
max-width: 1200px; |
|
|
} |
|
|
.config-panel { |
|
|
background-color: #f8f9fa; |
|
|
border-radius: 8px; |
|
|
padding: 15px; |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
.input-panel { |
|
|
border-right: 1px solid #e0e0e0; |
|
|
padding-right: 20px; |
|
|
} |
|
|
.output-panel { |
|
|
padding-left: 20px; |
|
|
} |
|
|
""" |
|
|
|
|
|
with gr.Blocks(css=css, title="Omni API Chat Interface") as interface: |
|
|
gr.Markdown("# π€ Omni API Chat Interface") |
|
|
gr.Markdown("Interact with the Omni API using text, PDFs, images, and audio files") |
|
|
|
|
|
|
|
|
with gr.Group(elem_classes=["config-panel"]): |
|
|
gr.Markdown("## βοΈ Configuration") |
|
|
with gr.Row(): |
|
|
api_endpoint = gr.Dropdown( |
|
|
label="API Endpoint", |
|
|
choices=list(API_ENDPOINTS.keys()), |
|
|
value="https://api.modelharbor.com" |
|
|
) |
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(scale=3): |
|
|
model = gr.Dropdown( |
|
|
label="Model", |
|
|
choices=API_ENDPOINTS["https://api.modelharbor.com"], |
|
|
value="qwen/qwen3-235b-a22b-instruct-2507", |
|
|
interactive=True |
|
|
) |
|
|
with gr.Column(scale=2): |
|
|
max_tokens = gr.Number( |
|
|
label="Max Tokens", |
|
|
value=16384, |
|
|
minimum=1, |
|
|
maximum=32000 |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Row(): |
|
|
|
|
|
with gr.Column(scale=1, elem_classes=["input-panel"]): |
|
|
gr.Markdown("## π Input") |
|
|
|
|
|
text_input = gr.Textbox( |
|
|
label="Your Message", |
|
|
placeholder="Type your message here...", |
|
|
lines=5 |
|
|
) |
|
|
|
|
|
file_upload = gr.File( |
|
|
label="Upload Files", |
|
|
file_count="multiple", |
|
|
file_types=[ |
|
|
".pdf", ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", |
|
|
".mp3", ".wav", ".m4a", ".flac", ".ogg" |
|
|
] |
|
|
) |
|
|
|
|
|
with gr.Row(): |
|
|
send_btn = gr.Button("π Send Request", variant="primary", size="lg") |
|
|
clear_btn = gr.Button("ποΈ Clear", variant="secondary") |
|
|
|
|
|
|
|
|
with gr.Column(scale=1, elem_classes=["output-panel"]): |
|
|
gr.Markdown("## π€ Response") |
|
|
|
|
|
status_output = gr.Textbox( |
|
|
label="Status", |
|
|
placeholder="Response status will appear here...", |
|
|
lines=8, |
|
|
max_lines=15, |
|
|
interactive=False |
|
|
) |
|
|
|
|
|
response_output = gr.Code( |
|
|
label="Raw Response", |
|
|
language="json", |
|
|
interactive=False |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Accordion("π Usage Examples", open=False): |
|
|
gr.Markdown(""" |
|
|
### Example Requests: |
|
|
|
|
|
**Text Only:** |
|
|
- Message: "Hello, how are you?" |
|
|
- Files: None |
|
|
|
|
|
**PDF Analysis:** |
|
|
- Message: "Please summarize this document" |
|
|
- Files: document.pdf |
|
|
|
|
|
**Image OCR:** |
|
|
- Message: "Extract text from this image" |
|
|
- Files: receipt.jpg |
|
|
|
|
|
**Audio Transcription:** |
|
|
- Message: "Transcribe this audio file" |
|
|
- Files: meeting.mp3 |
|
|
|
|
|
**Multi-modal:** |
|
|
- Message: "Analyze these files and provide insights" |
|
|
- Files: report.pdf, chart.png, recording.wav |
|
|
|
|
|
### Supported File Types: |
|
|
- **PDFs**: .pdf |
|
|
- **Images**: .jpg, .jpeg, .png, .gif, .bmp, .webp |
|
|
- **Audio**: .mp3, .wav, .m4a, .flac, .ogg |
|
|
""") |
|
|
|
|
|
|
|
|
send_btn.click( |
|
|
fn=send_request, |
|
|
inputs=[api_endpoint, model, max_tokens, text_input, file_upload], |
|
|
outputs=[status_output, response_output] |
|
|
) |
|
|
|
|
|
clear_btn.click( |
|
|
fn=clear_form, |
|
|
outputs=[text_input, status_output, response_output, file_upload] |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
text_input.submit( |
|
|
fn=send_request, |
|
|
inputs=[api_endpoint, model, max_tokens, text_input, file_upload], |
|
|
outputs=[status_output, response_output] |
|
|
) |
|
|
|
|
|
|
|
|
api_endpoint.change( |
|
|
fn=fetch_models, |
|
|
inputs=[api_endpoint], |
|
|
outputs=[model] |
|
|
) |
|
|
|
|
|
return interface |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
|
|
|
demo = create_ui() |
|
|
|
|
|
|
|
|
demo.launch( |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
) |