- Implementation of specialist execution api, including SSE protocol
- eveai_chat becomes deprecated and should be replaced with SSE - Adaptation of STANDARD_RAG specialist - Base class definition allowing to realise specialists with crewai framework - Implementation of SPIN_SPECIALIST - Implementation of test app for testing specialists (test_specialist_client). Also serves as an example for future SSE-based client - Improvements to startup scripts to better handle and scale multiple connections - Small improvements to the interaction forms and views - Caching implementation improved and augmented with additional caches
This commit is contained in:
247
tests/interactive_client/specialist_client.py
Normal file
247
tests/interactive_client/specialist_client.py
Normal file
@@ -0,0 +1,247 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
import requests # Used for calling the auth API
|
||||
from datetime import datetime
|
||||
import yaml # For loading the YAML configuration
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import socketio # Official python-socketio client
|
||||
|
||||
# ----------------------------
|
||||
# Constants for authentication and specialist selection
|
||||
# ----------------------------
|
||||
API_KEY = "EveAI-8342-2966-4731-6578-1010-8903-4230-4378"
|
||||
TENANT_ID = 2
|
||||
SPECIALIST_ID = 2
|
||||
BASE_API_URL = "http://macstudio.ask-eve-ai-local.com:8080/api/api/v1"
|
||||
BASE_SOCKET_URL = "http://macstudio.ask-eve-ai-local.com:8080"
|
||||
CONFIG_FILE = "config/specialists/SPIN_SPECIALIST/1.0.0.yaml" # Path to specialist configuration
|
||||
|
||||
# ----------------------------
|
||||
# Logging Configuration
|
||||
# ----------------------------
|
||||
LOG_FILENAME = "specialist_client.log"
|
||||
logging.basicConfig(
|
||||
filename=LOG_FILENAME,
|
||||
level=logging.DEBUG,
|
||||
format="%(asctime)s %(levelname)s: %(message)s"
|
||||
)
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_handler.setLevel(logging.INFO)
|
||||
logging.getLogger('').addHandler(console_handler)
|
||||
|
||||
# ----------------------------
|
||||
# Create the Socket.IO client using the official python-socketio client
|
||||
# ----------------------------
|
||||
sio = socketio.Client(logger=True, engineio_logger=True)
|
||||
room = None # Global variable to store the assigned room
|
||||
|
||||
# ----------------------------
|
||||
# Event Handlers
|
||||
# ----------------------------
|
||||
@sio.event
|
||||
def connect():
|
||||
logging.info("Connected to Socket.IO server.")
|
||||
print("Connected to server.")
|
||||
|
||||
@sio.event
|
||||
def disconnect():
|
||||
logging.info("Disconnected from Socket.IO server.")
|
||||
print("Disconnected from server.")
|
||||
|
||||
@sio.on("connect_error")
|
||||
def on_connect_error(data):
|
||||
logging.error("Connect error: %s", data)
|
||||
print("Connect error:", data)
|
||||
|
||||
@sio.on("authenticated")
|
||||
def on_authenticated(data):
|
||||
global room
|
||||
room = data.get("room")
|
||||
logging.info("Authenticated. Room: %s", room)
|
||||
print("Authenticated. Room:", room)
|
||||
|
||||
@sio.on("room_join")
|
||||
def on_room_join(data):
|
||||
global room
|
||||
room = data.get("room")
|
||||
logging.info("Room join event received. Room: %s", room)
|
||||
print("Joined room:", room)
|
||||
|
||||
@sio.on("token_expired")
|
||||
def on_token_expired(data):
|
||||
logging.warning("Token expired.")
|
||||
print("Token expired. Please refresh your session.")
|
||||
|
||||
@sio.on("reconnect_attempt")
|
||||
def on_reconnect_attempt(attempt):
|
||||
logging.info("Reconnect attempt #%s", attempt)
|
||||
print(f"Reconnect attempt #{attempt}")
|
||||
|
||||
@sio.on("reconnect")
|
||||
def on_reconnect():
|
||||
logging.info("Reconnected successfully.")
|
||||
print("Reconnected to server.")
|
||||
|
||||
@sio.on("reconnect_failed")
|
||||
def on_reconnect_failed():
|
||||
logging.error("Reconnection failed.")
|
||||
print("Reconnection failed. Please refresh.")
|
||||
|
||||
@sio.on("room_rejoin_result")
|
||||
def on_room_rejoin_result(data):
|
||||
if data.get("success"):
|
||||
global room
|
||||
room = data.get("room")
|
||||
logging.info("Successfully rejoined room: %s", room)
|
||||
print("Rejoined room:", room)
|
||||
else:
|
||||
logging.error("Failed to rejoin room.")
|
||||
print("Failed to rejoin room.")
|
||||
|
||||
@sio.on("bot_response")
|
||||
def on_bot_response(data):
|
||||
logging.info("Received bot response: %s", data)
|
||||
print("Bot response received:")
|
||||
print(json.dumps(data, indent=2))
|
||||
|
||||
@sio.on("task_status")
|
||||
def on_task_status(data):
|
||||
logging.info("Received task status: %s", data)
|
||||
print("Task status:")
|
||||
print(json.dumps(data, indent=2))
|
||||
|
||||
# ----------------------------
|
||||
# Helper: Retrieve token from REST API
|
||||
# ----------------------------
|
||||
def retrieve_token(api_url: str) -> str:
|
||||
payload = {
|
||||
"tenant_id": TENANT_ID,
|
||||
"api_key": API_KEY
|
||||
}
|
||||
try:
|
||||
logging.info("Requesting token from %s with payload: %s", api_url, payload)
|
||||
response = requests.post(api_url, json=payload)
|
||||
response.raise_for_status()
|
||||
token = response.json()["access_token"]
|
||||
logging.info("Token retrieved successfully.")
|
||||
return token
|
||||
except Exception as e:
|
||||
logging.error("Failed to retrieve token: %s", e)
|
||||
raise e
|
||||
|
||||
# ----------------------------
|
||||
# Main Interactive UI Function
|
||||
# ----------------------------
|
||||
def main():
|
||||
global room
|
||||
|
||||
# Retrieve the token
|
||||
auth_url = f"{BASE_API_URL}/auth/token"
|
||||
try:
|
||||
token = retrieve_token(auth_url)
|
||||
print("Token retrieved successfully.")
|
||||
except Exception as e:
|
||||
print("Error retrieving token. Check logs for details.")
|
||||
sys.exit(1)
|
||||
|
||||
# Parse the BASE_SOCKET_URL
|
||||
parsed_url = urlparse(BASE_SOCKET_URL)
|
||||
host_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
||||
|
||||
# Connect to the Socket.IO server.
|
||||
# Note: Use `auth` instead of `query_string` (the official client uses the `auth` parameter)
|
||||
try:
|
||||
sio.connect(
|
||||
host_url,
|
||||
socketio_path='/chat/socket.io',
|
||||
auth={"token": token},
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error("Failed to connect to Socket.IO server: %s", e)
|
||||
print("Failed to connect to Socket.IO server:", e)
|
||||
sys.exit(1)
|
||||
|
||||
# Allow time for authentication and room assignment.
|
||||
time.sleep(2)
|
||||
if not room:
|
||||
logging.warning("No room assigned. Exiting.")
|
||||
print("No room assigned by the server. Exiting.")
|
||||
sio.disconnect()
|
||||
sys.exit(1)
|
||||
|
||||
# Load specialist configuration from YAML.
|
||||
try:
|
||||
with open(CONFIG_FILE, "r") as f:
|
||||
specialist_config = yaml.safe_load(f)
|
||||
arg_config = specialist_config.get("arguments", {})
|
||||
logging.info("Loaded specialist argument configuration: %s", arg_config)
|
||||
except Exception as e:
|
||||
logging.error("Failed to load specialist configuration: %s", e)
|
||||
print("Failed to load specialist configuration. Exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
# Dictionary to store default values for static arguments (except "query")
|
||||
static_defaults = {}
|
||||
|
||||
print("\nInteractive Specialist Client")
|
||||
print("For each iteration, you will be prompted for the following arguments:")
|
||||
for key, details in arg_config.items():
|
||||
print(f" - {details.get('name', key)}: {details.get('description', '')}")
|
||||
print("Type 'quit' or 'exit' as the query to end the session.\n")
|
||||
|
||||
# Interactive loop: prompt for arguments and send user message.
|
||||
while True:
|
||||
current_arguments = {}
|
||||
for arg_key, arg_details in arg_config.items():
|
||||
prompt_msg = f"Enter {arg_details.get('name', arg_key)}"
|
||||
desc = arg_details.get("description", "")
|
||||
if desc:
|
||||
prompt_msg += f" ({desc})"
|
||||
if arg_key != "query":
|
||||
default_value = static_defaults.get(arg_key, "")
|
||||
if default_value:
|
||||
prompt_msg += f" [default: {default_value}]"
|
||||
prompt_msg += ": "
|
||||
value = input(prompt_msg).strip()
|
||||
if not value:
|
||||
value = default_value
|
||||
static_defaults[arg_key] = value
|
||||
else:
|
||||
prompt_msg += " (required): "
|
||||
value = input(prompt_msg).strip()
|
||||
while not value:
|
||||
print("Query is required. Please enter a value.")
|
||||
value = input(prompt_msg).strip()
|
||||
current_arguments[arg_key] = value
|
||||
|
||||
if current_arguments.get("query", "").lower() in ["quit", "exit"]:
|
||||
break
|
||||
|
||||
try:
|
||||
timezone = datetime.now().astimezone().tzname()
|
||||
except Exception:
|
||||
timezone = "UTC"
|
||||
|
||||
payload = {
|
||||
"token": token,
|
||||
"tenant_id": TENANT_ID,
|
||||
"specialist_id": SPECIALIST_ID,
|
||||
"arguments": current_arguments,
|
||||
"timezone": timezone,
|
||||
"room": room
|
||||
}
|
||||
|
||||
logging.info("Sending user_message with payload: %s", payload)
|
||||
print("Sending message to specialist...")
|
||||
sio.emit("user_message", payload)
|
||||
time.sleep(1)
|
||||
|
||||
print("Exiting interactive session.")
|
||||
sio.disconnect()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
225
tests/specialist_execution/test_specialist_client.py
Normal file
225
tests/specialist_execution/test_specialist_client.py
Normal file
@@ -0,0 +1,225 @@
|
||||
# test_specialist_client.py
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime
|
||||
import sseclient
|
||||
from typing import Dict, Any
|
||||
import yaml
|
||||
import os
|
||||
from termcolor import colored
|
||||
import sys
|
||||
|
||||
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
sys.path.append(project_root)
|
||||
|
||||
# Configuration Constants
|
||||
API_BASE_URL = "http://macstudio.ask-eve-ai-local.com:8080/api/api/v1"
|
||||
TENANT_ID = 2 # Replace with your tenant ID
|
||||
API_KEY = "EveAI-5096-5466-6143-1487-8085-4174-2080-7208" # Replace with your API key
|
||||
SPECIALIST_TYPE = "SPIN_SPECIALIST" # Replace with your specialist type
|
||||
SPECIALIST_ID = 5 # Replace with your specialist ID
|
||||
ROOT_FOLDER = "../.."
|
||||
|
||||
|
||||
def get_auth_token() -> str:
|
||||
"""Get authentication token from API"""
|
||||
response = requests.post(
|
||||
f"{API_BASE_URL}/auth/token",
|
||||
json={
|
||||
"tenant_id": TENANT_ID,
|
||||
"api_key": API_KEY
|
||||
}
|
||||
)
|
||||
print(colored(f"Status Code: {response.status_code}", "cyan"))
|
||||
print(colored(f"Response Headers: {response.headers}", "cyan"))
|
||||
print(colored(f"Response Content: {response.text}", "cyan"))
|
||||
if response.status_code == 200:
|
||||
return response.json()['access_token']
|
||||
else:
|
||||
raise Exception(f"Authentication failed: {response.text}")
|
||||
|
||||
|
||||
def get_session_id(auth_token: str) -> str:
|
||||
"""Get a new session ID from the API"""
|
||||
headers = {'Authorization': f'Bearer {auth_token}'}
|
||||
response = requests.get(
|
||||
f"{API_BASE_URL}/specialist-execution/start_session",
|
||||
headers=headers
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()["session_id"]
|
||||
|
||||
|
||||
def load_specialist_config() -> Dict[str, Any]:
|
||||
"""Load specialist configuration from YAML file"""
|
||||
config_path = f"{ROOT_FOLDER}/config/specialists/{SPECIALIST_TYPE}/1.0.0.yaml"
|
||||
if not os.path.exists(config_path):
|
||||
print(colored(f"Error: Configuration file not found: {config_path}", "red"))
|
||||
sys.exit(1)
|
||||
|
||||
with open(config_path, 'r') as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
|
||||
def get_argument_value(arg_name: str, arg_config: Dict[str, Any], previous_value: Any = None) -> Any:
|
||||
"""Get argument value from user input"""
|
||||
arg_type = arg_config.get('type', 'str')
|
||||
description = arg_config.get('description', '')
|
||||
|
||||
# Show previous value if it exists
|
||||
previous_str = f" (previous: {previous_value})" if previous_value is not None else ""
|
||||
|
||||
while True:
|
||||
print(colored(f"\n{arg_name}: {description}{previous_str}", "cyan"))
|
||||
value = input(colored("Enter value (or press Enter for previous): ", "yellow"))
|
||||
|
||||
if not value and previous_value is not None:
|
||||
return previous_value
|
||||
|
||||
try:
|
||||
if arg_type == 'int':
|
||||
return int(value)
|
||||
elif arg_type == 'float':
|
||||
return float(value)
|
||||
elif arg_type == 'bool':
|
||||
return value.lower() in ('true', 'yes', '1', 't')
|
||||
else:
|
||||
return value
|
||||
except ValueError:
|
||||
print(colored(f"Invalid input for type {arg_type}. Please try again.", "red"))
|
||||
|
||||
|
||||
def get_specialist_arguments(config: Dict[str, Any], previous_args: Dict[str, Any] = None) -> Dict[str, Any]:
|
||||
"""Get all required arguments for specialist execution"""
|
||||
arguments = {}
|
||||
previous_args = previous_args or {}
|
||||
|
||||
for arg_name, arg_config in config.get('arguments', {}).items():
|
||||
previous_value = previous_args.get(arg_name)
|
||||
arguments[arg_name] = get_argument_value(arg_name, arg_config, previous_value)
|
||||
|
||||
return arguments
|
||||
|
||||
|
||||
def process_specialist_updates(task_id: str, auth_token: str):
|
||||
"""Process SSE updates from specialist execution"""
|
||||
headers = {'Authorization': f'Bearer {auth_token}'}
|
||||
url = f"{API_BASE_URL}/specialist-execution/{task_id}/stream"
|
||||
|
||||
print(colored("\nConnecting to execution stream...", "cyan"))
|
||||
|
||||
with requests.get(url, headers=headers, stream=True) as response:
|
||||
response.raise_for_status()
|
||||
|
||||
for line in response.iter_lines():
|
||||
if not line:
|
||||
continue
|
||||
|
||||
line = line.decode('utf-8')
|
||||
if not line.startswith('data: '):
|
||||
continue
|
||||
|
||||
# Extract the data part
|
||||
data = line[6:] # Skip 'data: '
|
||||
|
||||
try:
|
||||
update = json.loads(data)
|
||||
update_type = update['processing_type']
|
||||
data = update['data']
|
||||
timestamp = update.get('timestamp', datetime.now().isoformat())
|
||||
|
||||
# Print updates in different colors based on type
|
||||
if update_type.endswith('Start'):
|
||||
print(colored(f"\n[{timestamp}] {update_type}: {data}", "blue"))
|
||||
elif update_type == 'EveAI Specialist Error':
|
||||
print(colored(f"\n[{timestamp}] Error: {data}", "red"))
|
||||
break
|
||||
elif update_type == 'EveAI Specialist Complete':
|
||||
print(colored(f"\n[{timestamp}] {update_type}: {data}", "green"))
|
||||
print(colored(f"\n[{timestamp}] {type(data)}", "green"))
|
||||
print(colored("Full Results:\n", "grey"))
|
||||
formatted_data = json.dumps(data, indent=4)
|
||||
print(colored(formatted_data, "grey"))
|
||||
print(colored("Answer:\n", "cyan"))
|
||||
answer = data.get('result', {}).get('rag_output', {}).get('answer', "")
|
||||
print(colored(answer, "cyan"))
|
||||
break
|
||||
elif update_type.endswith('Complete'):
|
||||
print(colored(f"\n[{timestamp}] {update_type}: {data}", "green"))
|
||||
else:
|
||||
print(colored(f"\n[{timestamp}] {update_type}: {data.get('message', '')}", "white"))
|
||||
except json.JSONDecodeError:
|
||||
print(colored(f"Error decoding message: {data}", "red"))
|
||||
except Exception as e:
|
||||
print(colored(f"Error processing message: {str(e)}", "red"))
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
# Get authentication token
|
||||
print(colored("Getting authentication token...", "cyan"))
|
||||
auth_token = get_auth_token()
|
||||
|
||||
# Load specialist configuration
|
||||
print(colored(f"Loading specialist configuration {SPECIALIST_TYPE}", "cyan"))
|
||||
config = load_specialist_config()
|
||||
previous_args = None
|
||||
|
||||
while True:
|
||||
try:
|
||||
# Get new session ID
|
||||
print(colored("Getting session ID...", "cyan"))
|
||||
session_id = get_session_id(auth_token)
|
||||
print(colored(f"New session ID: {session_id}", "cyan"))
|
||||
|
||||
# Get arguments
|
||||
arguments = get_specialist_arguments(config, previous_args)
|
||||
previous_args = arguments
|
||||
|
||||
# Start specialist execution
|
||||
print(colored("\nStarting specialist execution...", "cyan"))
|
||||
headers = {
|
||||
'Authorization': f'Bearer {auth_token}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
response = requests.post(
|
||||
f"{API_BASE_URL}/specialist-execution",
|
||||
headers=headers,
|
||||
json={
|
||||
'specialist_id': SPECIALIST_ID,
|
||||
'arguments': arguments,
|
||||
'session_id': session_id,
|
||||
'user_timezone': 'UTC'
|
||||
}
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
execution_data = response.json()
|
||||
task_id = execution_data['task_id']
|
||||
print(colored(f"Execution queued with Task ID: {task_id}", "cyan"))
|
||||
|
||||
# Process updates
|
||||
process_specialist_updates(task_id, auth_token)
|
||||
|
||||
# Ask if user wants to continue
|
||||
if input(colored("\nRun another execution? (y/n): ", "yellow")).lower() != 'y':
|
||||
break
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print(colored("\nExecution cancelled by user", "yellow"))
|
||||
if input(colored("Run another execution? (y/n): ", "yellow")).lower() != 'y':
|
||||
break
|
||||
except requests.exceptions.HTTPError as e:
|
||||
print(colored(f"\nHTTP Error: {e.response.status_code} - {e.response.text}", "red"))
|
||||
if input(colored("Try again? (y/n): ", "yellow")).lower() != 'y':
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
print(colored(f"\nError: {str(e)}", "red"))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user