- Move from OpenAI to Mistral Embeddings

- Move embedding model settings from tenant to catalog
- BUG: error processing configuration for chunking patterns in HTML_PROCESSOR
- Removed eveai_chat from docker-files and nginx configuration, as it is now obsolete
- BUG: error in Library Operations when creating a new default RAG library
- BUG: Added public type in migration scripts
- Removed SocketIO from all code and requirements.txt
This commit is contained in:
Josako
2025-02-25 11:17:19 +01:00
parent c037d4135e
commit 55a89c11bb
34 changed files with 457 additions and 444 deletions

View File

@@ -25,6 +25,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Security ### Security
- In case of vulnerabilities. - In case of vulnerabilities.
## [2.1.0-alfa]
### Added
- Zapier Refresh Document
- SPIN Specialist definition - from start to finish
- Introduction of startup scripts in eveai_app
- Caching for all configurations added
- Caching for processed specialist configurations
- Caching for specialist history
- Augmented Specialist Editor, including Specialist graphic presentation
- Introduction of specialist_execution_api, introducting SSE
- Introduction of crewai framework for specialist implementation
- Test app for testing specialists - also serves as a sample client application for SSE
-
### Changed
- Improvement of startup of applications using gevent, and better handling and scaling of multiple connections
- STANDARD_RAG Specialist improvement
-
### Deprecated
- eveai_chat - using sockets - will be replaced with new specialist_execution_api and SSE
## [2.0.1-alfa] ## [2.0.1-alfa]
### Added ### Added

View File

View File

@@ -0,0 +1,11 @@
from abc import abstractmethod
from typing import List
class EveAIEmbeddings:
@abstractmethod
def embed_documents(self, texts: List[str]) -> List[List[float]]:
pass
def embed_query(self, text: str) -> List[float]:
return self.embed_documents([text])[0]

View File

@@ -0,0 +1,40 @@
from flask import current_app
from langchain_mistralai import MistralAIEmbeddings
from typing import List, Any
import time
from common.eveai_model.eveai_embedding_base import EveAIEmbeddings
from common.utils.business_event_context import current_event
from mistralai import Mistral
class TrackedMistralAIEmbeddings(EveAIEmbeddings):
def __init__(self, model: str = "mistral_embed"):
api_key = current_app.config['MISTRAL_API_KEY']
self.client = Mistral(
api_key=api_key
)
self.model = model
super().__init__()
def embed_documents(self, texts: list[str]) -> list[list[float]]:
start_time = time.time()
result = self.client.embeddings.create(
model=self.model,
inputs=texts
)
end_time = time.time()
metrics = {
'total_tokens': result.usage.total_tokens,
'prompt_tokens': result.usage.prompt_tokens, # For embeddings, all tokens are prompt tokens
'completion_tokens': result.usage.completion_tokens,
'time_elapsed': end_time - start_time,
'interaction_type': 'Embedding',
}
current_event.log_llm_metrics(metrics)
embeddings = [embedding.embedding for embedding in result.data]
return embeddings

View File

@@ -5,7 +5,6 @@ from flask_security import Security
from flask_mailman import Mail from flask_mailman import Mail
from flask_login import LoginManager from flask_login import LoginManager
from flask_cors import CORS from flask_cors import CORS
from flask_socketio import SocketIO
from flask_jwt_extended import JWTManager from flask_jwt_extended import JWTManager
from flask_session import Session from flask_session import Session
from flask_wtf import CSRFProtect from flask_wtf import CSRFProtect
@@ -27,7 +26,6 @@ security = Security()
mail = Mail() mail = Mail()
login_manager = LoginManager() login_manager = LoginManager()
cors = CORS() cors = CORS()
socketio = SocketIO()
jwt = JWTManager() jwt = JWTManager()
session = Session() session = Session()
api_rest = Api() api_rest = Api()

View File

@@ -12,8 +12,10 @@ class Catalog(db.Model):
description = db.Column(db.Text, nullable=True) description = db.Column(db.Text, nullable=True)
type = db.Column(db.String(50), nullable=False, default="STANDARD_CATALOG") type = db.Column(db.String(50), nullable=False, default="STANDARD_CATALOG")
min_chunk_size = db.Column(db.Integer, nullable=True, default=2000) embedding_model = db.Column(db.String(50), nullable=True)
max_chunk_size = db.Column(db.Integer, nullable=True, default=3000)
min_chunk_size = db.Column(db.Integer, nullable=True, default=1500)
max_chunk_size = db.Column(db.Integer, nullable=True, default=2500)
# Meta Data # Meta Data
user_metadata = db.Column(JSONB, nullable=True) user_metadata = db.Column(JSONB, nullable=True)

View File

@@ -31,7 +31,6 @@ class Tenant(db.Model):
allowed_languages = db.Column(ARRAY(sa.String(2)), nullable=True) allowed_languages = db.Column(ARRAY(sa.String(2)), nullable=True)
# LLM specific choices # LLM specific choices
embedding_model = db.Column(db.String(50), nullable=True)
llm_model = db.Column(db.String(50), nullable=True) llm_model = db.Column(db.String(50), nullable=True)
# Entitlements # Entitlements
@@ -66,7 +65,6 @@ class Tenant(db.Model):
'type': self.type, 'type': self.type,
'default_language': self.default_language, 'default_language': self.default_language,
'allowed_languages': self.allowed_languages, 'allowed_languages': self.allowed_languages,
'embedding_model': self.embedding_model,
'llm_model': self.llm_model, 'llm_model': self.llm_model,
'currency': self.currency, 'currency': self.currency,
} }

View File

@@ -652,12 +652,15 @@ def json_to_patterns(json_content: str) -> str:
def json_to_pattern_list(json_content: str) -> list: def json_to_pattern_list(json_content: str) -> list:
"""Convert JSON patterns list to text area content""" """Convert JSON patterns list to text area content"""
try: try:
if json_content:
patterns = json.loads(json_content) patterns = json.loads(json_content)
if not isinstance(patterns, list): if not isinstance(patterns, list):
raise ValueError("JSON must contain a list of patterns") raise ValueError("JSON must contain a list of patterns")
# Unescape if needed # Unescape if needed
patterns = [pattern.replace('\\\\', '\\') for pattern in patterns] patterns = [pattern.replace('\\\\', '\\') for pattern in patterns]
return patterns return patterns
else:
return []
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON format: {e}") raise ValueError(f"Invalid JSON format: {e}")

View File

@@ -125,3 +125,14 @@ class EveAISocketInputException(EveAIException):
def __init__(self, message, status_code=400, payload=None): def __init__(self, message, status_code=400, payload=None):
super.__init__(message, status_code, payload) super.__init__(message, status_code, payload)
class EveAIInvalidEmbeddingModel(EveAIException):
"""Raised when no or an invalid embedding model is provided in the catalog"""
def __init__(self, tenant_id, catalog_id, status_code=400, payload=None):
self.tenant_id = tenant_id
self.catalog_id = catalog_id
# Construct the message dynamically
message = f"Tenant with ID '{tenant_id}' has no or an invalid embedding model in Catalog {catalog_id}."
super().__init__(message, status_code, payload)

View File

@@ -1,23 +1,25 @@
import os import os
from typing import Dict, Any, Optional from typing import Dict, Any, Optional, Tuple
import langcodes import langcodes
from langchain_core.language_models import BaseChatModel
from common.langchain.llm_metrics_handler import LLMMetricsHandler from common.langchain.llm_metrics_handler import LLMMetricsHandler
from common.langchain.templates.template_manager import TemplateManager from langchain_openai import ChatOpenAI
from langchain_openai import OpenAIEmbeddings, ChatOpenAI, OpenAI
from langchain_anthropic import ChatAnthropic from langchain_anthropic import ChatAnthropic
from langchain_mistralai import ChatMistralAI
from flask import current_app from flask import current_app
from datetime import datetime as dt, timezone as tz
from common.langchain.tracked_openai_embeddings import TrackedOpenAIEmbeddings from common.eveai_model.tracked_mistral_embeddings import TrackedMistralAIEmbeddings
from common.langchain.tracked_transcription import TrackedOpenAITranscription from common.langchain.tracked_transcription import TrackedOpenAITranscription
from common.models.user import Tenant from common.models.user import Tenant
from common.utils.cache.base import CacheHandler
from config.model_config import MODEL_CONFIG from config.model_config import MODEL_CONFIG
from common.extensions import template_manager, cache_manager from common.extensions import template_manager
from common.models.document import EmbeddingLargeOpenAI, EmbeddingSmallOpenAI from common.models.document import EmbeddingMistral
from common.utils.eveai_exceptions import EveAITenantNotFound from common.utils.eveai_exceptions import EveAITenantNotFound, EveAIInvalidEmbeddingModel
llm_model_cache: Dict[Tuple[str, float], BaseChatModel] = {}
llm_metrics_handler = LLMMetricsHandler()
def create_language_template(template: str, language: str) -> str: def create_language_template(template: str, language: str) -> str:
@@ -55,6 +57,63 @@ def replace_variable_in_template(template: str, variable: str, value: str) -> st
return template.replace(variable, value or "") return template.replace(variable, value or "")
def get_embedding_model_and_class(tenant_id, catalog_id, full_embedding_name):
"""
Retrieve the embedding model and embedding model class to store Embeddings
Args:
tenant_id: ID of the tenant
catalog_id: ID of the catalog
full_embedding_name: The full name of the embedding model: <provider>.<model>
Returns:
embedding_model, embedding_model_class
"""
embedding_provider, embedding_model_name = full_embedding_name.split('.')
# Calculate the embedding model to be used
if embedding_provider == "mistral":
api_key = current_app.config['MISTRAL_API_KEY']
embedding_model = TrackedMistralAIEmbeddings(
model=embedding_model_name
)
else:
raise EveAIInvalidEmbeddingModel(tenant_id, catalog_id)
# Calculate the Embedding Model Class to be used to store embeddings
if embedding_model_name == "mistral-embed":
embedding_model_class = EmbeddingMistral
else:
raise EveAIInvalidEmbeddingModel(tenant_id, catalog_id)
return embedding_model, embedding_model_class
def get_llm(full_model_name, temperature):
if not full_model_name:
full_model_name = 'openai.gpt-4o' # Default to gpt-4o for now, as this is the original model developed against
llm = llm_model_cache.get((full_model_name, temperature))
if not llm:
llm_provider, llm_model_name = full_model_name.split('.')
if llm_provider == "openai":
llm = ChatOpenAI(
api_key=current_app.config['OPENAI_API_KEY'],
model=llm_model_name,
temperature=temperature,
callbacks=[llm_metrics_handler]
)
elif llm_provider == "mistral":
llm = ChatMistralAI(
api_key=current_app.config['MISTRAL_API_KEY'],
model=llm_model_name,
temperature=temperature,
callbacks=[llm_metrics_handler]
)
llm_model_cache[(full_model_name, temperature)] = llm
class ModelVariables: class ModelVariables:
"""Manages model-related variables and configurations""" """Manages model-related variables and configurations"""
@@ -63,15 +122,13 @@ class ModelVariables:
Initialize ModelVariables with tenant and optional template manager Initialize ModelVariables with tenant and optional template manager
Args: Args:
tenant: Tenant instance tenant_id: Tenant instance
template_manager: Optional TemplateManager instance variables: Optional variables
""" """
current_app.logger.info(f'Model variables initialized with tenant {tenant_id} and variables \n{variables}') current_app.logger.info(f'Model variables initialized with tenant {tenant_id} and variables \n{variables}')
self.tenant_id = tenant_id self.tenant_id = tenant_id
self._variables = variables if variables is not None else self._initialize_variables() self._variables = variables if variables is not None else self._initialize_variables()
current_app.logger.info(f'Model _variables initialized to {self._variables}') current_app.logger.info(f'Model _variables initialized to {self._variables}')
self._embedding_model = None
self._embedding_model_class = None
self._llm_instances = {} self._llm_instances = {}
self.llm_metrics_handler = LLMMetricsHandler() self.llm_metrics_handler = LLMMetricsHandler()
self._transcription_model = None self._transcription_model = None
@@ -85,7 +142,6 @@ class ModelVariables:
raise EveAITenantNotFound(self.tenant_id) raise EveAITenantNotFound(self.tenant_id)
# Set model providers # Set model providers
variables['embedding_provider'], variables['embedding_model'] = tenant.embedding_model.split('.')
variables['llm_provider'], variables['llm_model'] = tenant.llm_model.split('.') variables['llm_provider'], variables['llm_model'] = tenant.llm_model.split('.')
variables['llm_full_model'] = tenant.llm_model variables['llm_full_model'] = tenant.llm_model
@@ -102,28 +158,6 @@ class ModelVariables:
return variables return variables
@property
def embedding_model(self):
"""Get the embedding model instance"""
if self._embedding_model is None:
api_key = os.getenv('OPENAI_API_KEY')
self._embedding_model = TrackedOpenAIEmbeddings(
api_key=api_key,
model=self._variables['embedding_model']
)
return self._embedding_model
@property
def embedding_model_class(self):
"""Get the embedding model class"""
if self._embedding_model_class is None:
if self._variables['embedding_model'] == 'text-embedding-3-large':
self._embedding_model_class = EmbeddingLargeOpenAI
else: # text-embedding-3-small
self._embedding_model_class = EmbeddingSmallOpenAI
return self._embedding_model_class
@property @property
def annotation_chunk_length(self): def annotation_chunk_length(self):
return self._variables['annotation_chunk_length'] return self._variables['annotation_chunk_length']

View File

@@ -13,14 +13,12 @@ def set_tenant_session_data(sender, user, **kwargs):
tenant = Tenant.query.filter_by(id=user.tenant_id).first() tenant = Tenant.query.filter_by(id=user.tenant_id).first()
session['tenant'] = tenant.to_dict() session['tenant'] = tenant.to_dict()
session['default_language'] = tenant.default_language session['default_language'] = tenant.default_language
session['default_embedding_model'] = tenant.embedding_model
session['default_llm_model'] = tenant.llm_model session['default_llm_model'] = tenant.llm_model
def clear_tenant_session_data(sender, user, **kwargs): def clear_tenant_session_data(sender, user, **kwargs):
session.pop('tenant', None) session.pop('tenant', None)
session.pop('default_language', None) session.pop('default_language', None)
session.pop('default_embedding_model', None)
session.pop('default_llm_model', None) session.pop('default_llm_model', None)

View File

@@ -63,8 +63,10 @@ class Config(object):
SUPPORTED_CURRENCIES = ['', '$'] SUPPORTED_CURRENCIES = ['', '$']
# supported LLMs # supported LLMs
SUPPORTED_EMBEDDINGS = ['openai.text-embedding-3-small', 'openai.text-embedding-3-large', 'mistral.mistral-embed'] # SUPPORTED_EMBEDDINGS = ['openai.text-embedding-3-small', 'openai.text-embedding-3-large', 'mistral.mistral-embed']
SUPPORTED_LLMS = ['openai.gpt-4o', 'anthropic.claude-3-5-sonnet', 'openai.gpt-4o-mini'] SUPPORTED_EMBEDDINGS = ['mistral.mistral-embed']
SUPPORTED_LLMS = ['openai.gpt-4o', 'anthropic.claude-3-5-sonnet', 'openai.gpt-4o-mini',
'mistral.mistral-large-latest', 'mistral.mistral-small-latest']
ANTHROPIC_LLM_VERSIONS = {'claude-3-5-sonnet': 'claude-3-5-sonnet-20240620', } ANTHROPIC_LLM_VERSIONS = {'claude-3-5-sonnet': 'claude-3-5-sonnet-20240620', }
@@ -75,13 +77,10 @@ class Config(object):
'anthropic.claude-3-5-sonnet': 8000 'anthropic.claude-3-5-sonnet': 8000
} }
# OpenAI API Keys # Environemnt Loaders
OPENAI_API_KEY = environ.get('OPENAI_API_KEY') OPENAI_API_KEY = environ.get('OPENAI_API_KEY')
MISTRAL_API_KEY = environ.get('MISTRAL_API_KEY')
# Groq API Keys
GROQ_API_KEY = environ.get('GROQ_API_KEY') GROQ_API_KEY = environ.get('GROQ_API_KEY')
# Anthropic API Keys
ANTHROPIC_API_KEY = environ.get('ANTHROPIC_API_KEY') ANTHROPIC_API_KEY = environ.get('ANTHROPIC_API_KEY')
# Celery settings # Celery settings
@@ -93,7 +92,7 @@ class Config(object):
# SocketIO settings # SocketIO settings
# SOCKETIO_ASYNC_MODE = 'threading' # SOCKETIO_ASYNC_MODE = 'threading'
SOCKETIO_ASYNC_MODE = 'gevent' # SOCKETIO_ASYNC_MODE = 'gevent'
# Session Settings # Session Settings
SESSION_TYPE = 'redis' SESSION_TYPE = 'redis'
@@ -207,13 +206,13 @@ class DevConfig(Config):
# UNSTRUCTURED_FULL_URL = 'https://flowitbv-16c4us0m.api.unstructuredapp.io/general/v0/general' # UNSTRUCTURED_FULL_URL = 'https://flowitbv-16c4us0m.api.unstructuredapp.io/general/v0/general'
# SocketIO settings # SocketIO settings
SOCKETIO_MESSAGE_QUEUE = f'{REDIS_BASE_URI}/1' # SOCKETIO_MESSAGE_QUEUE = f'{REDIS_BASE_URI}/1'
SOCKETIO_CORS_ALLOWED_ORIGINS = '*' # SOCKETIO_CORS_ALLOWED_ORIGINS = '*'
SOCKETIO_LOGGER = True # SOCKETIO_LOGGER = True
SOCKETIO_ENGINEIO_LOGGER = True # SOCKETIO_ENGINEIO_LOGGER = True
SOCKETIO_PING_TIMEOUT = 20000 # SOCKETIO_PING_TIMEOUT = 20000
SOCKETIO_PING_INTERVAL = 25000 # SOCKETIO_PING_INTERVAL = 25000
SOCKETIO_MAX_IDLE_TIME = timedelta(minutes=60) # Changing this value ==> change maxConnectionDuration value in # SOCKETIO_MAX_IDLE_TIME = timedelta(minutes=60) # Changing this value ==> change maxConnectionDuration value in
# eveai-chat-widget.js # eveai-chat-widget.js
# Google Cloud settings # Google Cloud settings
@@ -299,13 +298,13 @@ class ProdConfig(Config):
SESSION_REDIS = redis.from_url(f'{REDIS_BASE_URI}/2') SESSION_REDIS = redis.from_url(f'{REDIS_BASE_URI}/2')
# SocketIO settings # SocketIO settings
SOCKETIO_MESSAGE_QUEUE = f'{REDIS_BASE_URI}/1' # SOCKETIO_MESSAGE_QUEUE = f'{REDIS_BASE_URI}/1'
SOCKETIO_CORS_ALLOWED_ORIGINS = '*' # SOCKETIO_CORS_ALLOWED_ORIGINS = '*'
SOCKETIO_LOGGER = True # SOCKETIO_LOGGER = True
SOCKETIO_ENGINEIO_LOGGER = True # SOCKETIO_ENGINEIO_LOGGER = True
SOCKETIO_PING_TIMEOUT = 20000 # SOCKETIO_PING_TIMEOUT = 20000
SOCKETIO_PING_INTERVAL = 25000 # SOCKETIO_PING_INTERVAL = 25000
SOCKETIO_MAX_IDLE_TIME = timedelta(minutes=60) # Changing this value ==> change maxConnectionDuration value in # SOCKETIO_MAX_IDLE_TIME = timedelta(minutes=60) # Changing this value ==> change maxConnectionDuration value in
# eveai-chat-widget.js # eveai-chat-widget.js
# Google Cloud settings # Google Cloud settings

View File

@@ -13,6 +13,7 @@ content: |
HTML is between triple backquotes. HTML is between triple backquotes.
```{html}``` ```{html}```
model: "mistral.mistral-small-latest"
metadata: metadata:
author: "Josako" author: "Josako"
date_added: "2024-11-10" date_added: "2024-11-10"

View File

@@ -28,6 +28,7 @@ x-common-variables: &common-variables
FLOWER_PASSWORD: 'Jungles' FLOWER_PASSWORD: 'Jungles'
OPENAI_API_KEY: 'sk-proj-8R0jWzwjL7PeoPyMhJTZT3BlbkFJLb6HfRB2Hr9cEVFWEhU7' OPENAI_API_KEY: 'sk-proj-8R0jWzwjL7PeoPyMhJTZT3BlbkFJLb6HfRB2Hr9cEVFWEhU7'
GROQ_API_KEY: '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' GROQ_API_KEY: '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'
MISTRAL_API_KEY: 'jGDc6fkCbt0iOC0jQsbuZhcjLWBPGc2b'
ANTHROPIC_API_KEY: 'sk-ant-api03-c2TmkzbReeGhXBO5JxNH6BJNylRDonc9GmZd0eRbrvyekec2' ANTHROPIC_API_KEY: 'sk-ant-api03-c2TmkzbReeGhXBO5JxNH6BJNylRDonc9GmZd0eRbrvyekec2'
JWT_SECRET_KEY: 'bsdMkmQ8ObfMD52yAFg4trrvjgjMhuIqg2fjDpD/JqvgY0ccCcmlsEnVFmR79WPiLKEA3i8a5zmejwLZKl4v9Q==' JWT_SECRET_KEY: 'bsdMkmQ8ObfMD52yAFg4trrvjgjMhuIqg2fjDpD/JqvgY0ccCcmlsEnVFmR79WPiLKEA3i8a5zmejwLZKl4v9Q=='
API_ENCRYPTION_KEY: 'xfF5369IsredSrlrYZqkM9ZNrfUASYYS6TCcAR9UKj4=' API_ENCRYPTION_KEY: 'xfF5369IsredSrlrYZqkM9ZNrfUASYYS6TCcAR9UKj4='
@@ -65,7 +66,7 @@ services:
- ./logs/nginx:/var/log/nginx - ./logs/nginx:/var/log/nginx
depends_on: depends_on:
- eveai_app - eveai_app
- eveai_chat - eveai_api
networks: networks:
- eveai-network - eveai-network
@@ -134,39 +135,39 @@ services:
networks: networks:
- eveai-network - eveai-network
eveai_chat: # eveai_chat:
image: josakola/eveai_chat:latest # image: josakola/eveai_chat:latest
build: # build:
context: .. # context: ..
dockerfile: ./docker/eveai_chat/Dockerfile # dockerfile: ./docker/eveai_chat/Dockerfile
platforms: # platforms:
- linux/amd64 # - linux/amd64
- linux/arm64 # - linux/arm64
ports: # ports:
- 5002:5002 # - 5002:5002
environment: # environment:
<<: *common-variables # <<: *common-variables
COMPONENT_NAME: eveai_chat # COMPONENT_NAME: eveai_chat
volumes: # volumes:
- ../eveai_chat:/app/eveai_chat # - ../eveai_chat:/app/eveai_chat
- ../common:/app/common # - ../common:/app/common
- ../config:/app/config # - ../config:/app/config
- ../scripts:/app/scripts # - ../scripts:/app/scripts
- ../patched_packages:/app/patched_packages # - ../patched_packages:/app/patched_packages
- ./eveai_logs:/app/logs # - ./eveai_logs:/app/logs
depends_on: # depends_on:
db: # db:
condition: service_healthy # condition: service_healthy
redis: # redis:
condition: service_healthy # condition: service_healthy
healthcheck: # healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost:5002/healthz/ready" ] # Adjust based on your health endpoint # test: [ "CMD", "curl", "-f", "http://localhost:5002/healthz/ready" ] # Adjust based on your health endpoint
interval: 30s # interval: 30s
timeout: 1s # timeout: 1s
retries: 3 # retries: 3
start_period: 30s # start_period: 30s
networks: # networks:
- eveai-network # - eveai-network
eveai_chat_workers: eveai_chat_workers:
image: josakola/eveai_chat_workers:latest image: josakola/eveai_chat_workers:latest

View File

@@ -31,6 +31,7 @@ x-common-variables: &common-variables
OPENAI_API_KEY: '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' OPENAI_API_KEY: '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'
GROQ_API_KEY: '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' GROQ_API_KEY: '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'
ANTHROPIC_API_KEY: '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' ANTHROPIC_API_KEY: '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'
MISTRAL_API_KEY: 'PjnUeDRPD7B144wdHlH0CzR7m0z8RHXi'
JWT_SECRET_KEY: '0d99e810e686ea567ef305d8e9b06195c4db482952e19276590a726cde60a408' JWT_SECRET_KEY: '0d99e810e686ea567ef305d8e9b06195c4db482952e19276590a726cde60a408'
API_ENCRYPTION_KEY: 'Ly5XYWwEKiasfAwEqdEMdwR-k0vhrq6QPYd4whEROB0=' API_ENCRYPTION_KEY: 'Ly5XYWwEKiasfAwEqdEMdwR-k0vhrq6QPYd4whEROB0='
GRAYLOG_HOST: de4zvu.stackhero-network.com GRAYLOG_HOST: de4zvu.stackhero-network.com
@@ -66,7 +67,7 @@ services:
- "traefik.http.services.nginx.loadbalancer.server.port=80" - "traefik.http.services.nginx.loadbalancer.server.port=80"
depends_on: depends_on:
- eveai_app - eveai_app
- eveai_chat - eveai_api
networks: networks:
- eveai-network - eveai-network
@@ -99,23 +100,23 @@ services:
networks: networks:
- eveai-network - eveai-network
eveai_chat: # eveai_chat:
platform: linux/amd64 # platform: linux/amd64
image: josakola/eveai_chat:latest # image: josakola/eveai_chat:latest
ports: # ports:
- 5002:5002 # - 5002:5002
environment: # environment:
<<: *common-variables # <<: *common-variables
COMPONENT_NAME: eveai_chat # COMPONENT_NAME: eveai_chat
volumes: # volumes:
- eveai_logs:/app/logs # - eveai_logs:/app/logs
healthcheck: # healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost:5002/healthz/ready" ] # Adjust based on your health endpoint # test: [ "CMD", "curl", "-f", "http://localhost:5002/healthz/ready" ] # Adjust based on your health endpoint
interval: 10s # interval: 10s
timeout: 5s # timeout: 5s
retries: 5 # retries: 5
networks: # networks:
- eveai-network # - eveai-network
eveai_chat_workers: eveai_chat_workers:
platform: linux/amd64 platform: linux/amd64

View File

@@ -5,7 +5,7 @@ from flask_jwt_extended import get_jwt_identity, verify_jwt_in_request
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from werkzeug.exceptions import HTTPException from werkzeug.exceptions import HTTPException
from common.extensions import db, api_rest, jwt, minio_client, simple_encryption, cors from common.extensions import db, api_rest, jwt, minio_client, simple_encryption, cors, cache_manager
import os import os
import logging.config import logging.config
@@ -60,6 +60,9 @@ def create_app(config_file=None):
# Register Request Debugger # Register Request Debugger
register_request_debugger(app) register_request_debugger(app)
# Register Cache Handlers
register_cache_handlers(app)
@app.before_request @app.before_request
def check_cors(): def check_cors():
if request.method == 'OPTIONS': if request.method == 'OPTIONS':
@@ -120,6 +123,7 @@ def register_extensions(app):
jwt.init_app(app) jwt.init_app(app)
minio_client.init_app(app) minio_client.init_app(app)
simple_encryption.init_app(app) simple_encryption.init_app(app)
cache_manager.init_app(app)
cors.init_app(app, resources={ cors.init_app(app, resources={
r"/api/v1/*": { r"/api/v1/*": {
"origins": "*", "origins": "*",
@@ -201,3 +205,8 @@ def register_error_handlers(app):
"message": str(e), "message": str(e),
"type": "BadRequestError" "type": "BadRequestError"
}), 400 }), 400
def register_cache_handlers(app):
from common.utils.cache.config_cache import register_config_cache_handlers
register_config_cache_handlers(cache_manager)

View File

@@ -5,9 +5,11 @@ from flask import Response, stream_with_context, current_app
from flask_restx import Namespace, Resource, fields from flask_restx import Namespace, Resource, fields
from flask_jwt_extended import jwt_required, get_jwt_identity from flask_jwt_extended import jwt_required, get_jwt_identity
from common.extensions import cache_manager
from common.utils.celery_utils import current_celery from common.utils.celery_utils import current_celery
from common.utils.execution_progress import ExecutionProgressTracker from common.utils.execution_progress import ExecutionProgressTracker
from eveai_api.api.auth import requires_service from eveai_api.api.auth import requires_service
from common.models.interaction import Specialist
specialist_execution_ns = Namespace('specialist-execution', description='Specialist execution operations') specialist_execution_ns = Namespace('specialist-execution', description='Specialist execution operations')
@@ -87,3 +89,47 @@ class ExecutionStream(Resource):
'Connection': 'keep-alive' 'Connection': 'keep-alive'
} }
) )
specialist_arguments_input = specialist_execution_ns.model('SpecialistArgumentsInput', {
'specialist_id': fields.Integer(required=True, description='ID of the specialist to use'),
})
specialist_arguments_response = specialist_execution_ns.model('SpecialistArgumentsResponse', {
'arguments': fields.Raw(description='Dynamic list of attributes for the specialist.'),
})
@specialist_execution_ns.route('/specialist_arguments', methods=['GET'])
class SpecialistArgument(Resource):
@jwt_required()
@requires_service('SPECIALIST_API')
@specialist_execution_ns.expect(specialist_arguments_input)
@specialist_execution_ns.response(200, 'Specialist configuration fetched.', specialist_arguments_response)
@specialist_execution_ns.response(404, 'Specialist configuration not found.')
@specialist_execution_ns.response(500, 'Internal Server Error')
def get(self):
"""Start execution of a specialist"""
tenant_id = get_jwt_identity()
data = specialist_execution_ns.payload
specialist_id = data['specialist_id']
try:
specialist = Specialist.query.get(specialist_id)
if specialist:
configuration = cache_manager.specialists_config_cache.get_config(specialist.type,
specialist.type_version)
current_app.logger.debug(f"Configuration returned: {configuration}")
if configuration:
if 'arguments' in configuration:
return {
'arguments': configuration['arguments'],
}, 200
else:
specialist_execution_ns.abort(404, 'No arguments found in specialist configuration.')
else:
specialist_execution_ns.abort(404, 'Error fetching Specialist configuration.')
else:
specialist_execution_ns.abort(404, 'Error fetching Specialist')
except Exception as e:
current_app.logger.error(f"Error while retrieving Specialist configuration: {str(e)}")
specialist_execution_ns.abort(500, 'Unexpected Error while fetching Specialist configuration.')

View File

@@ -1,7 +1,7 @@
import logging import logging
import os import os
from flask import Flask, render_template, jsonify, flash, redirect, request from flask import Flask, jsonify
from flask_security import SQLAlchemyUserDatastore, LoginForm from flask_security import SQLAlchemyUserDatastore
from flask_security.signals import user_authenticated from flask_security.signals import user_authenticated
from werkzeug.middleware.proxy_fix import ProxyFix from werkzeug.middleware.proxy_fix import ProxyFix
import logging.config import logging.config
@@ -12,7 +12,6 @@ from common.models.user import User, Role, Tenant, TenantDomain
import common.models.interaction import common.models.interaction
import common.models.entitlements import common.models.entitlements
import common.models.document import common.models.document
from common.utils.nginx_utils import prefixed_url_for
from common.utils.startup_eveai import perform_startup_actions from common.utils.startup_eveai import perform_startup_actions
from config.logging_config import LOGGING from config.logging_config import LOGGING
from common.utils.security import set_tenant_session_data from common.utils.security import set_tenant_session_data

View File

@@ -11,7 +11,7 @@ When you change chunking of embedding information, you'll need to manually refre
{% block content %} {% block content %}
<form method="post"> <form method="post">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{% set disabled_fields = ['type'] %} {% set disabled_fields = ['type', 'embedding_model'] %}
{% set exclude_fields = [] %} {% set exclude_fields = [] %}
<!-- Render Static Fields --> <!-- Render Static Fields -->
{% for field in form.get_static_fields() %} {% for field in form.get_static_fields() %}

View File

@@ -9,7 +9,7 @@
{% block content %} {% block content %}
<form method="post"> <form method="post">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{% set disabled_fields = ['name', 'embedding_model', 'llm_model'] %} {% set disabled_fields = ['name', 'llm_model'] %}
{% set exclude_fields = [] %} {% set exclude_fields = [] %}
{% for field in form %} {% for field in form %}
{{ render_field(field, disabled_fields, exclude_fields) }} {{ render_field(field, disabled_fields, exclude_fields) }}

View File

@@ -35,7 +35,7 @@
<div class="tab-content tab-space"> <div class="tab-content tab-space">
<!-- Model Information Tab --> <!-- Model Information Tab -->
<div class="tab-pane fade show active" id="model-info-tab" role="tabpanel"> <div class="tab-pane fade show active" id="model-info-tab" role="tabpanel">
{% set model_fields = ['embedding_model', 'llm_model'] %} {% set model_fields = ['llm_model'] %}
{% for field in form %} {% for field in form %}
{{ render_included_field(field, disabled_fields=model_fields, include_fields=model_fields) }} {{ render_included_field(field, disabled_fields=model_fields, include_fields=model_fields) }}
{% endfor %} {% endfor %}

View File

@@ -1,6 +1,7 @@
from flask import session, current_app from flask import session, current_app
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import (StringField, BooleanField, SubmitField, DateField, IntegerField, SelectField, TextAreaField, URLField) from wtforms import (StringField, BooleanField, SubmitField, DateField, IntegerField, SelectField, TextAreaField,
URLField)
from wtforms.validators import DataRequired, Length, Optional, URL, ValidationError, NumberRange from wtforms.validators import DataRequired, Length, Optional, URL, ValidationError, NumberRange
from flask_wtf.file import FileField, FileRequired from flask_wtf.file import FileField, FileRequired
import json import json
@@ -30,10 +31,13 @@ class CatalogForm(FlaskForm):
# Select Field for Catalog Type (Uses the CATALOG_TYPES defined in config) # Select Field for Catalog Type (Uses the CATALOG_TYPES defined in config)
type = SelectField('Catalog Type', validators=[DataRequired()]) type = SelectField('Catalog Type', validators=[DataRequired()])
min_chunk_size = IntegerField('Minimum Chunk Size (2000)', validators=[NumberRange(min=0), Optional()], # Selection fields for processing & creating embeddings
default=2000) embedding_model = SelectField('Embedding Model', choices=[], validators=[DataRequired()])
max_chunk_size = IntegerField('Maximum Chunk Size (3000)', validators=[NumberRange(min=0), Optional()],
default=3000) min_chunk_size = IntegerField('Minimum Chunk Size (1500)', validators=[NumberRange(min=0), Optional()],
default=1500)
max_chunk_size = IntegerField('Maximum Chunk Size (2500)', validators=[NumberRange(min=0), Optional()],
default=2500)
# Metadata fields # Metadata fields
user_metadata = TextAreaField('User Metadata', validators=[Optional(), validate_json]) user_metadata = TextAreaField('User Metadata', validators=[Optional(), validate_json])
@@ -43,6 +47,7 @@ class CatalogForm(FlaskForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Dynamically populate the 'type' field using the constructor # Dynamically populate the 'type' field using the constructor
self.type.choices = [(key, value['name']) for key, value in CATALOG_TYPES.items()] self.type.choices = [(key, value['name']) for key, value in CATALOG_TYPES.items()]
self.embedding_model.choices = [(model, model) for model in current_app.config['SUPPORTED_EMBEDDINGS']]
class EditCatalogForm(DynamicFormBase): class EditCatalogForm(DynamicFormBase):
@@ -52,6 +57,9 @@ class EditCatalogForm(DynamicFormBase):
# Select Field for Catalog Type (Uses the CATALOG_TYPES defined in config) # Select Field for Catalog Type (Uses the CATALOG_TYPES defined in config)
type = StringField('Catalog Type', validators=[DataRequired()], render_kw={'readonly': True}) type = StringField('Catalog Type', validators=[DataRequired()], render_kw={'readonly': True})
# Selection fields for processing & creating embeddings
embedding_model = StringField('Embedding Model', validators=[DataRequired()], render_kw={'readonly': True})
min_chunk_size = IntegerField('Minimum Chunk Size (2000)', validators=[NumberRange(min=0), Optional()], min_chunk_size = IntegerField('Minimum Chunk Size (2000)', validators=[NumberRange(min=0), Optional()],
default=2000) default=2000)
max_chunk_size = IntegerField('Maximum Chunk Size (3000)', validators=[NumberRange(min=0), Optional()], max_chunk_size = IntegerField('Maximum Chunk Size (3000)', validators=[NumberRange(min=0), Optional()],

View File

@@ -684,8 +684,9 @@ def create_default_rag_library():
name='Default RAG Catalog', name='Default RAG Catalog',
description='Default RAG Catalog', description='Default RAG Catalog',
type="STANDARD_CATALOG", type="STANDARD_CATALOG",
min_chunk_size=2000, min_chunk_size=1500,
max_chunk_size=3000, max_chunk_size=2500,
embedding_model="mistral.mistral-embed"
) )
set_logging_information(cat, timestamp) set_logging_information(cat, timestamp)
@@ -696,7 +697,7 @@ def create_default_rag_library():
name='Default HTML Processor', name='Default HTML Processor',
description='Default HTML Processor', description='Default HTML Processor',
catalog_id=cat.id, catalog_id=cat.id,
type="HTML Processor", type="HTML_PROCESSOR",
configuration={ configuration={
"html_tags": "p, h1, h2, h3, h4, h5, h6, li, table, thead, tbody, tr, td", "html_tags": "p, h1, h2, h3, h4, h5, h6, li, table, thead, tbody, tr, td",
"html_end_tags": "p, li, table", "html_end_tags": "p, li, table",

View File

@@ -21,7 +21,6 @@ class TenantForm(FlaskForm):
# Timezone # Timezone
timezone = SelectField('Timezone', choices=[], validators=[DataRequired()]) timezone = SelectField('Timezone', choices=[], validators=[DataRequired()])
# LLM fields # LLM fields
embedding_model = SelectField('Embedding Model', choices=[], validators=[DataRequired()])
llm_model = SelectField('Large Language Model', choices=[], validators=[DataRequired()]) llm_model = SelectField('Large Language Model', choices=[], validators=[DataRequired()])
# Embedding variables # Embedding variables
submit = SubmitField('Submit') submit = SubmitField('Submit')
@@ -36,7 +35,6 @@ class TenantForm(FlaskForm):
# initialise timezone # initialise timezone
self.timezone.choices = [(tz, tz) for tz in pytz.all_timezones] self.timezone.choices = [(tz, tz) for tz in pytz.all_timezones]
# initialise LLM fields # initialise LLM fields
self.embedding_model.choices = [(model, model) for model in current_app.config['SUPPORTED_EMBEDDINGS']]
self.llm_model.choices = [(model, model) for model in current_app.config['SUPPORTED_LLMS']] self.llm_model.choices = [(model, model) for model in current_app.config['SUPPORTED_LLMS']]
# Initialize fallback algorithms # Initialize fallback algorithms
self.type.choices = [(t, t) for t in current_app.config['TENANT_TYPES']] self.type.choices = [(t, t) for t in current_app.config['TENANT_TYPES']]

View File

@@ -228,7 +228,6 @@ def handle_tenant_selection():
# set tenant information in the session # set tenant information in the session
session['tenant'] = the_tenant.to_dict() session['tenant'] = the_tenant.to_dict()
session['default_language'] = the_tenant.default_language session['default_language'] = the_tenant.default_language
session['embedding_model'] = the_tenant.embedding_model
session['llm_model'] = the_tenant.llm_model session['llm_model'] = the_tenant.llm_model
# remove catalog-related items from the session # remove catalog-related items from the session
session.pop('catalog_id', None) session.pop('catalog_id', None)

View File

@@ -10,7 +10,7 @@ from common.extensions import db
from common.models.document import Document, DocumentVersion, Catalog, Retriever from common.models.document import Document, DocumentVersion, Catalog, Retriever
from common.models.user import Tenant from common.models.user import Tenant
from common.utils.datetime_utils import get_date_in_timezone from common.utils.datetime_utils import get_date_in_timezone
from common.utils.model_utils import get_model_variables from common.utils.model_utils import get_embedding_model_and_class
from .base import BaseRetriever from .base import BaseRetriever
from .registry import RetrieverRegistry from .registry import RetrieverRegistry
@@ -25,10 +25,10 @@ class StandardRAGRetriever(BaseRetriever):
retriever = Retriever.query.get_or_404(retriever_id) retriever = Retriever.query.get_or_404(retriever_id)
self.catalog_id = retriever.catalog_id self.catalog_id = retriever.catalog_id
self.tenant_id = tenant_id
self.similarity_threshold = retriever.configuration.get('es_similarity_threshold', 0.3) self.similarity_threshold = retriever.configuration.get('es_similarity_threshold', 0.3)
self.k = retriever.configuration.get('es_k', 8) self.k = retriever.configuration.get('es_k', 8)
self.tuning = retriever.tuning self.tuning = retriever.tuning
self.model_variables = get_model_variables(self.tenant_id)
self.log_tuning("Standard RAG retriever initialized") self.log_tuning("Standard RAG retriever initialized")
@@ -161,8 +161,9 @@ class StandardRAGRetriever(BaseRetriever):
def _get_query_embedding(self, query: str): def _get_query_embedding(self, query: str):
"""Get embedding for the query text""" """Get embedding for the query text"""
embedding_model = self.model_variables.embedding_model catalog = Catalog.query.get_or_404(self.catalog_id)
return embedding_model.embed_query(query) embedding_model, embedding_model_class = get_embedding_model_and_class(self.tenant_id, self.catalog_id,
catalog.embedding_model)
# Register the retriever type # Register the retriever type

View File

@@ -12,18 +12,20 @@ from langchain_core.runnables import RunnablePassthrough
from sqlalchemy import or_ from sqlalchemy import or_
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from common.extensions import db, minio_client from common.extensions import db
from common.models.document import DocumentVersion, Embedding, Document, Processor, Catalog from common.models.document import DocumentVersion, Embedding, Document, Processor, Catalog
from common.models.user import Tenant from common.models.user import Tenant
from common.utils.celery_utils import current_celery from common.utils.celery_utils import current_celery
from common.utils.database import Database from common.utils.database import Database
from common.utils.model_utils import create_language_template, get_model_variables from common.utils.model_utils import create_language_template, get_model_variables, get_embedding_model_and_class
from common.utils.business_event import BusinessEvent from common.utils.business_event import BusinessEvent
from common.utils.business_event_context import current_event from common.utils.business_event_context import current_event
from config.type_defs.processor_types import PROCESSOR_TYPES from config.type_defs.processor_types import PROCESSOR_TYPES
from eveai_workers.processors.processor_registry import ProcessorRegistry from eveai_workers.processors.processor_registry import ProcessorRegistry
from common.utils.eveai_exceptions import EveAIInvalidEmbeddingModel
from common.utils.config_field_types import json_to_pattern_list from common.utils.config_field_types import json_to_pattern_list
@@ -155,7 +157,7 @@ def embed_markdown(tenant, model_variables, document_version, catalog, processor
# Create embeddings # Create embeddings
with current_event.create_span("Create Embeddings"): with current_event.create_span("Create Embeddings"):
embeddings = embed_chunks(tenant, model_variables, document_version, enriched_chunks) embeddings = embed_chunks(tenant, catalog, document_version, enriched_chunks)
# Update document version and save embeddings # Update document version and save embeddings
try: try:
@@ -227,9 +229,14 @@ def summarize_chunk(tenant, model_variables, document_version, chunk):
raise raise
def embed_chunks(tenant, model_variables, document_version, chunks): def embed_chunks(tenant, catalog, document_version, chunks):
embedding_model = model_variables.embedding_model if catalog.embedding_model:
embedding_model, embedding_model_class = get_embedding_model_and_class(tenant.id, catalog.id,
catalog.embedding_model)
else:
raise EveAIInvalidEmbeddingModel(tenant.id, catalog.id)
# Actually embed
try: try:
embeddings = embedding_model.embed_documents(chunks) embeddings = embedding_model.embed_documents(chunks)
except LangChainException as e: except LangChainException as e:
@@ -241,7 +248,7 @@ def embed_chunks(tenant, model_variables, document_version, chunks):
# Add embeddings to the database # Add embeddings to the database
new_embeddings = [] new_embeddings = []
for chunk, embedding in zip(chunks, embeddings): for chunk, embedding in zip(chunks, embeddings):
new_embedding = model_variables.embedding_model_class() new_embedding = embedding_model_class()
new_embedding.document_version = document_version new_embedding.document_version = document_version
new_embedding.active = True new_embedding.active = True
new_embedding.chunk = chunk new_embedding.chunk = chunk
@@ -309,7 +316,7 @@ def combine_chunks_for_markdown(potential_chunks, min_chars, max_chars, processo
return False return False
chunking_patterns = json_to_pattern_list(processor.configuration.get('chunking_patterns', [])) chunking_patterns = json_to_pattern_list(processor.configuration.get('chunking_patterns', ""))
processor.log_tuning(f'Chunking Patterns Extraction: ', { processor.log_tuning(f'Chunking Patterns Extraction: ', {
'Full Configuration': processor.configuration, 'Full Configuration': processor.configuration,

View File

@@ -0,0 +1,32 @@
"""Move embedding model settings to Catalog
Revision ID: b02d9ad000f4
Revises: f0ab991a6411
Create Date: 2025-02-21 22:11:10.313148
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'b02d9ad000f4'
down_revision = 'f0ab991a6411'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('tenant', schema=None) as batch_op:
batch_op.drop_column('embedding_model')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('tenant', schema=None) as batch_op:
batch_op.add_column(sa.Column('embedding_model', sa.VARCHAR(length=50), autoincrement=False, nullable=True))
# ### end Alembic commands ###

View File

@@ -71,7 +71,7 @@ target_db = current_app.extensions['migrate'].db
def get_public_table_names(): def get_public_table_names():
# TODO: This function should include the necessary functionality to automatically retrieve table names # TODO: This function should include the necessary functionality to automatically retrieve table names
return ['role', 'roles_users', 'tenant', 'user', 'tenant_domain','license_tier', 'license', 'license_usage', return ['role', 'roles_users', 'tenant', 'user', 'tenant_domain','license_tier', 'license', 'license_usage',
'business_event_log'] 'business_event_log', 'tenant_project']
PUBLIC_TABLES = get_public_table_names() PUBLIC_TABLES = get_public_table_names()

View File

@@ -0,0 +1,29 @@
"""Link embedding models to Catalog
Revision ID: 2b04e961eee4
Revises: e58835fadd96
Create Date: 2025-02-21 22:06:43.527013
"""
from alembic import op
import sqlalchemy as sa
import pgvector
# revision identifiers, used by Alembic.
revision = '2b04e961eee4'
down_revision = 'e58835fadd96'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('catalog', sa.Column('embedding_model', sa.String(length=50), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('catalog', 'embedding_model')
# ### end Alembic commands ###

View File

@@ -74,24 +74,24 @@ http {
root /etc/nginx/public; root /etc/nginx/public;
} }
location /chat/ { # location /chat/ {
proxy_pass http://eveai_chat:5002/; # proxy_pass http://eveai_chat:5002/;
#
proxy_set_header Host $host; # proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; # proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; # proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1; # proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; # proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; # proxy_set_header Connection "upgrade";
proxy_buffering off; # proxy_buffering off;
#
# Add CORS headers # # Add CORS headers
add_header 'Access-Control-Allow-Origin' '*' always; # add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; # add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always; # add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always;
add_header 'Access-Control-Allow-Credentials' 'true' always; # add_header 'Access-Control-Allow-Credentials' 'true' always;
} # }
location /admin/ { location /admin/ {
# include uwsgi_params; # include uwsgi_params;

View File

@@ -1,4 +1,4 @@
alembic~=1.13.2 alembic~=1.14.1
annotated-types~=0.7.0 annotated-types~=0.7.0
bcrypt~=4.1.3 bcrypt~=4.1.3
beautifulsoup4~=4.12.3 beautifulsoup4~=4.12.3
@@ -6,18 +6,17 @@ celery~=5.4.0
certifi~=2024.7.4 certifi~=2024.7.4
chardet~=5.2.0 chardet~=5.2.0
cors~=1.0.1 cors~=1.0.1
Flask~=3.0.3 Flask~=3.1.0
Flask-BabelEx~=0.9.4 Flask-BabelEx~=0.9.4
Flask-Bootstrap~=3.3.7.1 Flask-Bootstrap~=3.3.7.1
Flask-Cors~=5.0.0 Flask-Cors~=5.0.0
Flask-JWT-Extended~=4.6.0 Flask-JWT-Extended~=4.7.1
Flask-Login~=0.6.3 Flask-Login~=0.6.3
flask-mailman~=1.1.1 flask-mailman~=1.1.1
Flask-Migrate~=4.0.7 Flask-Migrate~=4.1.0
Flask-Principal~=0.4.0 Flask-Principal~=0.4.0
Flask-Security-Too~=5.5.2 Flask-Security-Too~=5.6.0
Flask-Session~=0.8.0 Flask-Session~=0.8.0
Flask-SocketIO~=5.3.6
Flask-SQLAlchemy~=3.1.1 Flask-SQLAlchemy~=3.1.1
Flask-WTF~=1.2.1 Flask-WTF~=1.2.1
gevent~=24.2.1 gevent~=24.2.1
@@ -43,12 +42,10 @@ pgvector~=0.2.5
pycryptodome~=3.20.0 pycryptodome~=3.20.0
pydantic~=2.9.1 pydantic~=2.9.1
PyJWT~=2.8.0 PyJWT~=2.8.0
PySocks~=1.7.1
python-dateutil~=2.9.0.post0 python-dateutil~=2.9.0.post0
python-engineio~=4.9.1 python-engineio~=4.9.1
python-iso639~=2024.4.27 python-iso639~=2024.4.27
python-magic~=0.4.27 python-magic~=0.4.27
python-socketio~=5.11.3
pytz~=2024.1 pytz~=2024.1
PyYAML~=6.0.2 PyYAML~=6.0.2
redis~=5.0.4 redis~=5.0.4
@@ -64,7 +61,7 @@ groq~=0.9.0
pydub~=0.25.1 pydub~=0.25.1
argparse~=1.4.0 argparse~=1.4.0
minio~=7.2.7 minio~=7.2.7
Werkzeug~=3.0.3 Werkzeug~=3.1.3
itsdangerous~=2.2.0 itsdangerous~=2.2.0
cryptography~=43.0.0 cryptography~=43.0.0
graypy~=2.1.0 graypy~=2.1.0
@@ -92,3 +89,5 @@ python-docx~=1.1.2
crewai~=0.102.0 crewai~=0.102.0
sseclient~=0.0.27 sseclient~=0.0.27
termcolor~=2.5.0 termcolor~=2.5.0
mistral-common~=1.5.3
mistralai~=1.5.0

View File

@@ -1,247 +0,0 @@
#!/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()

View File

@@ -18,9 +18,7 @@ sys.path.append(project_root)
API_BASE_URL = "http://macstudio.ask-eve-ai-local.com:8080/api/api/v1" API_BASE_URL = "http://macstudio.ask-eve-ai-local.com:8080/api/api/v1"
TENANT_ID = 2 # Replace with your tenant ID TENANT_ID = 2 # Replace with your tenant ID
API_KEY = "EveAI-5096-5466-6143-1487-8085-4174-2080-7208" # Replace with your API key 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 SPECIALIST_ID = 5 # Replace with your specialist ID
ROOT_FOLDER = "../.."
def get_auth_token() -> str: def get_auth_token() -> str:
@@ -52,15 +50,27 @@ def get_session_id(auth_token: str) -> str:
return response.json()["session_id"] return response.json()["session_id"]
def load_specialist_config() -> Dict[str, Any]: def get_specialist_config(auth_token: str, specialist_id: int) -> Dict[str, Any]:
"""Load specialist configuration from YAML file""" """Get specialist configuration from API"""
config_path = f"{ROOT_FOLDER}/config/specialists/{SPECIALIST_TYPE}/1.0.0.yaml" headers = {
if not os.path.exists(config_path): 'Authorization': f'Bearer {auth_token}',
print(colored(f"Error: Configuration file not found: {config_path}", "red")) 'Content-Type': 'application/json'
sys.exit(1) }
with open(config_path, 'r') as f: response = requests.get(
return yaml.safe_load(f) f"{API_BASE_URL}/specialist-execution/specialist_arguments",
headers=headers,
json={
'specialist_id': specialist_id
}
)
print(colored(f"Status Code: {response.status_code}", "cyan"))
if response.status_code == 200:
config_data = response.json()
return config_data.get('arguments', {})
else:
raise Exception(f"Failed to get specialist configuration: {response.text}")
def get_argument_value(arg_name: str, arg_config: Dict[str, Any], previous_value: Any = None) -> Any: def get_argument_value(arg_name: str, arg_config: Dict[str, Any], previous_value: Any = None) -> Any:
@@ -163,8 +173,10 @@ def main():
auth_token = get_auth_token() auth_token = get_auth_token()
# Load specialist configuration # Load specialist configuration
print(colored(f"Loading specialist configuration {SPECIALIST_TYPE}", "cyan")) print(colored(f"Loading specialist configuration", "cyan"))
config = load_specialist_config() config = {
'arguments': get_specialist_config(auth_token, SPECIALIST_ID)
}
previous_args = None previous_args = None
while True: while True: