|
|
import gradio as gr
|
|
|
import requests
|
|
|
import json
|
|
|
import asyncio
|
|
|
import logging
|
|
|
from typing import Dict, List, Any, Optional
|
|
|
import anthropic
|
|
|
import openai
|
|
|
from datetime import datetime
|
|
|
import os
|
|
|
|
|
|
|
|
|
|
|
|
def get_api_keys():
|
|
|
|
|
|
openai_key = os.getenv("OPENAI_API_KEY")
|
|
|
dolibarr_key = os.getenv("DOLIBARR_API_KEY")
|
|
|
|
|
|
|
|
|
if not openai_key or not dolibarr_key:
|
|
|
from dotenv import load_dotenv
|
|
|
load_dotenv()
|
|
|
openai_key = os.getenv("OPENAI_API_KEY")
|
|
|
dolibarr_key = os.getenv("DOLIBARR_API_KEY")
|
|
|
|
|
|
|
|
|
if not openai_key:
|
|
|
raise ValueError("OPENAI_API_KEY not found in environment variables or .env file")
|
|
|
if not dolibarr_key:
|
|
|
raise ValueError("DOLIBARR_API_KEY not found in environment variables or .env file")
|
|
|
|
|
|
return openai_key, dolibarr_key
|
|
|
|
|
|
|
|
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class DolibarrAPI:
|
|
|
"""Your existing Dolibarr API class - keeping it unchanged"""
|
|
|
base_url = "https://valiant-trust-production.up.railway.app/api/index.php"
|
|
|
|
|
|
def __init__(self, api_key: str):
|
|
|
self.api_key = api_key
|
|
|
self.headers = {
|
|
|
'DOLAPIKEY': api_key,
|
|
|
'Content-Type': 'application/json',
|
|
|
'Accept': 'application/json'
|
|
|
}
|
|
|
|
|
|
def _request(self, method: str, endpoint: str, data: Optional[dict] = None, params: Optional[dict] = None) -> Any:
|
|
|
base_url = "https://valiant-trust-production.up.railway.app/api/index.php"
|
|
|
url = f"{base_url}{endpoint}"
|
|
|
|
|
|
try:
|
|
|
response = requests.request(method, url, headers=self.headers, json=data, params=params)
|
|
|
response.raise_for_status()
|
|
|
return response.json()
|
|
|
except requests.exceptions.RequestException as e:
|
|
|
logger.error(f"API request failed: {e}")
|
|
|
return {"error": f"API request failed: {str(e)}"}
|
|
|
except json.JSONDecodeError as e:
|
|
|
logger.error(f"JSON decode error: {e}")
|
|
|
return {"error": f"Invalid JSON response: {str(e)}"}
|
|
|
|
|
|
def get_req(self, endpoint: str, params: Optional[dict] = None):
|
|
|
return self._request('GET', endpoint, params=params)
|
|
|
|
|
|
def post_req(self, endpoint: str, params: dict):
|
|
|
return self._request("POST", endpoint, data=params)
|
|
|
|
|
|
def put_req(self, endpoint: str, params: dict):
|
|
|
return self._request("PUT", endpoint, data=params)
|
|
|
|
|
|
def del_req(self, endpoint: str, params: Optional[dict] = None):
|
|
|
return self._request("DELETE", endpoint, params=params)
|
|
|
|
|
|
def dolibarr_interface(method: str, endpoint: str, api_key=os.getenv("DOLIBARR_API_KEY"), payload_str: str = "") -> str:
|
|
|
"""Your existing interface function - keeping it unchanged"""
|
|
|
try:
|
|
|
api = DolibarrAPI(api_key)
|
|
|
method = method.upper()
|
|
|
|
|
|
payload = None
|
|
|
if payload_str and payload_str.strip():
|
|
|
try:
|
|
|
payload = json.loads(payload_str)
|
|
|
except json.JSONDecodeError as e:
|
|
|
return json.dumps({"error": f"Invalid JSON payload: {str(e)}"}, indent=2)
|
|
|
|
|
|
if method == 'GET':
|
|
|
result = api.get_req(endpoint, payload)
|
|
|
elif method == 'POST':
|
|
|
if not payload:
|
|
|
return json.dumps({"error": "POST requests require a payload"}, indent=2)
|
|
|
result = api.post_req(endpoint, payload)
|
|
|
elif method == 'PUT':
|
|
|
if not payload:
|
|
|
return json.dumps({"error": "PUT requests require a payload"}, indent=2)
|
|
|
result = api.put_req(endpoint, payload)
|
|
|
elif method == 'DELETE':
|
|
|
result = api.del_req(endpoint, payload)
|
|
|
else:
|
|
|
return json.dumps({"error": f"Invalid HTTP method '{method}' selected."}, indent=2)
|
|
|
|
|
|
return json.dumps(result, indent=2)
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Unexpected error in dolibarr_interface: {e}")
|
|
|
return json.dumps({"error": f"Unexpected error: {str(e)}"}, indent=2)
|
|
|
|
|
|
def format_api_response(api_result, max_items=10):
|
|
|
try:
|
|
|
data = json.loads(api_result)
|
|
|
if isinstance(data, list) and len(data) > max_items:
|
|
|
truncated = data[:max_items]
|
|
|
truncated.append({"info": f"Showing first {max_items} results. Ask for more if needed."})
|
|
|
return json.dumps(truncated, indent=2)
|
|
|
return api_result
|
|
|
except Exception:
|
|
|
return api_result
|
|
|
|
|
|
class OpenAIDolibarrAgent:
|
|
|
def __init__(self, openai_api_key: str, dolibarr_api_key: str, base_url: str = None):
|
|
|
self.client = openai.OpenAI(api_key=openai_api_key, base_url=base_url)
|
|
|
self.dolibarr_api_key = dolibarr_api_key
|
|
|
|
|
|
|
|
|
self.system_prompt = """### CRITICAL RULES
|
|
|
1. Show ALL data from API calls - never truncate unless asked
|
|
|
2. Display every record in structured tables
|
|
|
3. Never fabricate data
|
|
|
4. Be proactive - call correct API immediately
|
|
|
5. Confirm success for create/update operations
|
|
|
6. Show full details for specific IDs
|
|
|
7. Explain errors clearly
|
|
|
|
|
|
### API ENDPOINTS
|
|
|
- /thirdparties (Customers/Suppliers)
|
|
|
- /invoices
|
|
|
- /products
|
|
|
- /contacts
|
|
|
- /orders, /proposals, /bills, /stocks, /projects, /users
|
|
|
|
|
|
All endpoints support: GET (list/fetch), POST (create), PUT (update), DELETE
|
|
|
|
|
|
### REQUIRED FIELDS
|
|
|
|
|
|
**Create Thirdparty**
|
|
|
```json
|
|
|
{
|
|
|
"name": "John Doe",
|
|
|
"address": "123 Main St",
|
|
|
"zip": "12345",
|
|
|
"town": "Sample City",
|
|
|
"country_id": 1,
|
|
|
"email": "john@example.com",
|
|
|
"phone": "+123456789",
|
|
|
"type": 1,
|
|
|
"status": 1
|
|
|
}
|
|
|
```
|
|
|
|
|
|
**Create Invoice**
|
|
|
```json
|
|
|
{
|
|
|
"socid": 10,
|
|
|
"date": "2025-06-01",
|
|
|
"duedate": "2025-06-15",
|
|
|
"lines": [
|
|
|
{
|
|
|
"desc": "Service",
|
|
|
"subprice": 500,
|
|
|
"qty": 1,
|
|
|
"total_ht": 500,
|
|
|
"vat": 18,
|
|
|
"total_ttc": 590
|
|
|
}
|
|
|
]
|
|
|
}
|
|
|
```
|
|
|
|
|
|
**Create Product**
|
|
|
```json
|
|
|
{
|
|
|
"label": "Smartphone",
|
|
|
"price": 499.99,
|
|
|
"stock": 100,
|
|
|
"description": "Latest model",
|
|
|
"socid": 10
|
|
|
}
|
|
|
```
|
|
|
|
|
|
**Create Contact**
|
|
|
```json
|
|
|
{
|
|
|
"thirdparty_id": 1,
|
|
|
"firstname": "Jane",
|
|
|
"lastname": "Doe",
|
|
|
"email": "jane@example.com",
|
|
|
"phone": "+123456789",
|
|
|
"position": "Sales Manager",
|
|
|
"address": "123 Street"
|
|
|
}
|
|
|
```
|
|
|
|
|
|
### π§Ύ RESPONSE FORMAT
|
|
|
- For **lists**: display `ID`, `Name/Label`, `Status`, and any other key fields in **tables**.
|
|
|
- For **individual records**: show all fields in **structured format**.
|
|
|
|
|
|
- Prefix counts: e.g., **"Found 32 customers:"**
|
|
|
|
|
|
- On errors: explain clearly what failed, and why.
|
|
|
---
|
|
|
|
|
|
### βοΈ GENERAL RULE
|
|
|
When user mentions something like "get invoice", immediately call the respective endpoint (`GET /invoices`) and show **complete** results.
|
|
|
NEVER truncate unless user asks for filtered or paginated results.
|
|
|
|
|
|
Current date: """ + datetime.now().strftime("%Y-%m-%d")
|
|
|
|
|
|
|
|
|
self.functions = [
|
|
|
{
|
|
|
"name": "dolibarr_api",
|
|
|
"description": "Execute API calls to the Dolibarr ERP system",
|
|
|
"parameters": {
|
|
|
"type": "object",
|
|
|
"properties": {
|
|
|
"method": {
|
|
|
"type": "string",
|
|
|
"enum": ["GET", "POST", "PUT", "DELETE"],
|
|
|
"description": "HTTP method for the API call"
|
|
|
},
|
|
|
"endpoint": {
|
|
|
"type": "string",
|
|
|
"description": "API endpoint (e.g., /thirdparties, /invoices)"
|
|
|
},
|
|
|
"payload": {
|
|
|
"type": "string",
|
|
|
"description": "JSON payload for POST/PUT requests (leave empty for GET)"
|
|
|
}
|
|
|
},
|
|
|
"required": ["method", "endpoint"]
|
|
|
}
|
|
|
}
|
|
|
]
|
|
|
|
|
|
def execute_dolibarr_call(self, method: str, endpoint: str, payload: str = "") -> str:
|
|
|
"""Execute the actual Dolibarr API call"""
|
|
|
raw_result = dolibarr_interface(method, endpoint, self.dolibarr_api_key, payload)
|
|
|
return format_api_response(raw_result)
|
|
|
|
|
|
def chat(self, message: str, history: List[List[str]]) -> str:
|
|
|
"""Main chat function that processes user messages"""
|
|
|
try:
|
|
|
|
|
|
messages = [{"role": "system", "content": self.system_prompt}]
|
|
|
|
|
|
|
|
|
max_history = 6
|
|
|
for human_msg, assistant_msg in history[-max_history:]:
|
|
|
if human_msg:
|
|
|
messages.append({"role": "user", "content": human_msg})
|
|
|
if assistant_msg:
|
|
|
messages.append({"role": "assistant", "content": assistant_msg})
|
|
|
|
|
|
|
|
|
messages.append({"role": "user", "content": message})
|
|
|
|
|
|
|
|
|
logger.info("Sending request to Nebius API...")
|
|
|
response = self.client.chat.completions.create(
|
|
|
model="gpt-4.1-mini",
|
|
|
messages=messages,
|
|
|
functions=self.functions,
|
|
|
function_call="auto",
|
|
|
max_tokens=1500
|
|
|
)
|
|
|
|
|
|
|
|
|
message = response.choices[0].message
|
|
|
logger.info(f"Received response from Nebius: {message}")
|
|
|
|
|
|
if message.function_call:
|
|
|
|
|
|
function_name = message.function_call.name
|
|
|
function_args = json.loads(message.function_call.arguments)
|
|
|
logger.info(f"Function call: {function_name} with args: {function_args}")
|
|
|
|
|
|
if function_name == "dolibarr_api":
|
|
|
api_result = self.execute_dolibarr_call(
|
|
|
method=function_args.get("method", "GET"),
|
|
|
endpoint=function_args.get("endpoint", ""),
|
|
|
payload=function_args.get("payload", "")
|
|
|
)
|
|
|
logger.info(f"Dolibarr API result: {api_result}")
|
|
|
|
|
|
messages.append({
|
|
|
"role": "assistant",
|
|
|
"content": None,
|
|
|
"function_call": message.function_call
|
|
|
})
|
|
|
messages.append({
|
|
|
"role": "function",
|
|
|
"name": function_name,
|
|
|
"content": api_result
|
|
|
})
|
|
|
|
|
|
|
|
|
logger.info("Getting final response from Nebius...")
|
|
|
final_response = self.client.chat.completions.create(
|
|
|
model="gpt-4.1-mini",
|
|
|
messages=messages,
|
|
|
max_tokens=1500
|
|
|
)
|
|
|
logger.info(f"Final response: {final_response.choices[0].message}")
|
|
|
|
|
|
|
|
|
content = final_response.choices[0].message.content
|
|
|
|
|
|
content = content.split('</think>')[-1].strip() if '</think>' in content else content
|
|
|
return content
|
|
|
|
|
|
|
|
|
content = message.content
|
|
|
content = content.split('</think>')[-1].strip() if '</think>' in content else content
|
|
|
return content if content else "I couldn't process that request."
|
|
|
|
|
|
except openai.APIConnectionError as e:
|
|
|
logger.error(f"OpenAI API Connection Error: {e}")
|
|
|
return "Sorry, I'm having trouble connecting to OpenAI. Please check if the API key is valid and the service is available."
|
|
|
except openai.AuthenticationError as e:
|
|
|
logger.error(f"OpenAI API Authentication Error: {e}")
|
|
|
return "Sorry, there's an authentication error with the OpenAI API. Please check if the API key is correct."
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error in chat: {e}")
|
|
|
return f"Sorry, I encountered an error: {str(e)}"
|
|
|
|
|
|
def create_openai_agent_interface():
|
|
|
"""Create the Gradio interface for the OpenAI-powered Dolibarr agent"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
OPENAI_API_KEY, DOLIBARR_API_KEY = get_api_keys()
|
|
|
|
|
|
|
|
|
logger.info("API Keys loaded successfully")
|
|
|
logger.info(f"OpenAI API Key length: {len(OPENAI_API_KEY) if OPENAI_API_KEY else 0}")
|
|
|
logger.info(f"Dolibarr API Key length: {len(DOLIBARR_API_KEY) if DOLIBARR_API_KEY else 0}")
|
|
|
|
|
|
if not OPENAI_API_KEY or not DOLIBARR_API_KEY:
|
|
|
raise ValueError("API keys not found. Please set them in Hugging Face Secrets or .env file")
|
|
|
|
|
|
|
|
|
agent = OpenAIDolibarrAgent(OPENAI_API_KEY, DOLIBARR_API_KEY)
|
|
|
agent = OpenAIDolibarrAgent(OPENAI_API_KEY, DOLIBARR_API_KEY)
|
|
|
|
|
|
|
|
|
|
|
|
demo = gr.ChatInterface(
|
|
|
fn=agent.chat,
|
|
|
title="π€ ERP Assistant",
|
|
|
description="""
|
|
|
π€ AI-Powered Dolibarr ERP Assistant - Your intelligent business management companion. I can help you manage customers, invoices, products, orders, and financial operations through natural conversation. Simply type your request (e.g., "Show me all customers" or "Create a new invoice") and get instant results. Try it with our demo instance at https://valiant-trust-production.up.railway.app/ (username: admin, password: admin123).
|
|
|
- Check this out: https://youtu.be/oYAxRSNC8hc
|
|
|
""",
|
|
|
examples=[
|
|
|
"Show me all customers",
|
|
|
"List all invoices",
|
|
|
"What products do we have?",
|
|
|
"Get details for customer ID 1",
|
|
|
"Show me recent proposals"
|
|
|
],
|
|
|
cache_examples=False,
|
|
|
theme=gr.themes.Soft()
|
|
|
)
|
|
|
|
|
|
return demo
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
try:
|
|
|
print("π Starting OpenAI-Powered Dolibarr Agent...")
|
|
|
|
|
|
|
|
|
demo = create_openai_agent_interface()
|
|
|
demo.launch(
|
|
|
server_name="127.0.0.1",
|
|
|
server_port=7862,
|
|
|
share=False,
|
|
|
debug=True,
|
|
|
show_error=True
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Failed to start application: {e}")
|
|
|
print(f"β Error starting application: {e}")
|
|
|
|
|
|
|
|
|
"""
|
|
|
- "Show me all customers"
|
|
|
- "List all invoices"
|
|
|
- "Get me customer details for ID 1"
|
|
|
- "What products do we have?"
|
|
|
- "Show me recent proposals"
|
|
|
- "Create a new customer named Test Corp"
|
|
|
- "Find all unpaid invoices"
|
|
|
"""
|
|
|
|