- Finish editing of Specialists with overview, agent - task - tool editor

- Split differrent caching mechanisms (types, version tree, config) into different cachers
- Improve resource usage on starting components, and correct gevent usage
- Refine repopack usage for eveai_app (too large)
- Change nginx dockerfile to allow for specialist overviews being served statically
This commit is contained in:
Josako
2025-01-23 09:43:48 +01:00
parent 7bddeb0ebd
commit d106520d22
39 changed files with 1312 additions and 281 deletions

View File

@@ -13,6 +13,7 @@ migrations/
*material* *material*
*nucleo* *nucleo*
*package* *package*
*.svg
nginx/mime.types nginx/mime.types
*.gitignore* *.gitignore*
.python-version .python-version

View File

@@ -0,0 +1,28 @@
docker/
eveai_api/
eveai_beat/
eveai_chat/
eveai_chat_workers/
eveai_entitlements/
eveai_workers/
instance/
integrations/
migrations/
nginx/
scripts/
common/models/entitlements.py
common/models/interaction.py
common/models/user.py
config/agents/
config/prompts/
config/specialists/
config/tasks/
config/tools/
eveai_app/templates/administration/
eveai_app/templates/entitlements/
eveai_app/templates/interaction/
eveai_app/templates/user/
eveai_app/views/administration*
eveai_app/views/entitlements*
eveai_app/views/interaction*
eveai_app/views/user*

View File

@@ -0,0 +1,28 @@
docker/
eveai_api/
eveai_beat/
eveai_chat/
eveai_chat_workers/
eveai_entitlements/
eveai_workers/
instance/
integrations/
migrations/
nginx/
scripts/
common/models/document.py
common/models/interaction.py
common/models/user.py
config/agents/
config/prompts/
config/specialists/
config/tasks/
config/tools/
eveai_app/templates/administration/
eveai_app/templates/document/
eveai_app/templates/interaction/
eveai_app/templates/user/
eveai_app/views/administration*
eveai_app/views/document*
eveai_app/views/interaction*
eveai_app/views/user*

View File

@@ -0,0 +1,23 @@
docker/
eveai_api/
eveai_beat/
eveai_chat/
eveai_chat_workers/
eveai_entitlements/
eveai_workers/
instance/
integrations/
migrations/
nginx/
scripts/
common/models/entitlements.py
common/models/document.py
common/models/user.py
eveai_app/templates/administration/
eveai_app/templates/entitlements/
eveai_app/templates/document/
eveai_app/templates/user/
eveai_app/views/administration*
eveai_app/views/entitlements*
eveai_app/views/document*
eveai_app/views/user*

View File

@@ -0,0 +1,28 @@
docker/
eveai_api/
eveai_beat/
eveai_chat/
eveai_chat_workers/
eveai_entitlements/
eveai_workers/
instance/
integrations/
migrations/
nginx/
scripts/
common/models/entitlements.py
common/models/interaction.py
common/models/document.py
config/agents/
config/prompts/
config/specialists/
config/tasks/
config/tools/
eveai_app/templates/administration/
eveai_app/templates/entitlements/
eveai_app/templates/interaction/
eveai_app/templates/document/
eveai_app/views/administration*
eveai_app/views/entitlements*
eveai_app/views/interaction*
eveai_app/views/document*

View File

@@ -73,6 +73,7 @@ class EveAITask(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_RAG") type = db.Column(db.String(50), nullable=False, default="STANDARD_RAG")
type_version = db.Column(db.String(20), nullable=True, default="1.0.0") type_version = db.Column(db.String(20), nullable=True, default="1.0.0")
task_description = db.Column(db.Text, nullable=True)
expected_output = db.Column(db.Text, nullable=True) expected_output = db.Column(db.Text, nullable=True)
tuning = db.Column(db.Boolean, nullable=True, default=False) tuning = db.Column(db.Boolean, nullable=True, default=False)
configuration = db.Column(JSONB, nullable=True) configuration = db.Column(JSONB, nullable=True)

View File

@@ -1,7 +1,8 @@
from typing import Any, Dict, List, Optional, TypeVar, Generic, Type from typing import Any, Dict, List, Optional, TypeVar, Generic, Type
from dataclasses import dataclass from dataclasses import dataclass
from flask import Flask from flask import Flask, current_app
from dogpile.cache import CacheRegion from dogpile.cache import CacheRegion
from abc import ABC, abstractmethod
T = TypeVar('T') # Generic type parameter for cached data T = TypeVar('T') # Generic type parameter for cached data
@@ -47,6 +48,46 @@ class CacheHandler(Generic[T]):
self.prefix = prefix self.prefix = prefix
self._key_components = [] # List of required key components self._key_components = [] # List of required key components
@abstractmethod
def _to_cache_data(self, instance: T) -> Any:
"""
Convert the data to a cacheable format for internal use.
Args:
instance: The data to be cached.
Returns:
A serializable format of the instance.
"""
pass
@abstractmethod
def _from_cache_data(self, data: Any, **kwargs) -> T:
"""
Convert cached data back to usable format for internal use.
Args:
data: The cached data.
**kwargs: Additional context.
Returns:
The data in its usable format.
"""
pass
@abstractmethod
def _should_cache(self, value: T) -> bool:
"""
Validate if the value should be cached for internal use.
Args:
value: The value to be cached.
Returns:
True if the value should be cached, False otherwise.
"""
pass
def configure_keys(self, *components: str): def configure_keys(self, *components: str):
""" """
Configure required components for cache key generation. Configure required components for cache key generation.
@@ -77,8 +118,13 @@ class CacheHandler(Generic[T]):
if missing: if missing:
raise ValueError(f"Missing key components: {missing}") raise ValueError(f"Missing key components: {missing}")
region_name = getattr(self.region, 'name', 'default_region')
current_app.logger.debug(f"Generating cache key in region {region_name} with prefix {self.prefix} "
f"for {self._key_components}")
key = CacheKey({k: identifiers[k] for k in self._key_components}) key = CacheKey({k: identifiers[k] for k in self._key_components})
return f"{self.prefix}:{str(key)}" return f"{region_name}_{self.prefix}:{str(key)}"
def get(self, creator_func, **identifiers) -> T: def get(self, creator_func, **identifiers) -> T:
""" """
@@ -92,18 +138,19 @@ class CacheHandler(Generic[T]):
Cached or newly created value Cached or newly created value
""" """
cache_key = self.generate_key(**identifiers) cache_key = self.generate_key(**identifiers)
current_app.logger.debug(f"Cache key: {cache_key}")
def creator(): def creator():
instance = creator_func(**identifiers) instance = creator_func(**identifiers)
return self.to_cache_data(instance) return self._to_cache_data(instance)
cached_data = self.region.get_or_create( cached_data = self.region.get_or_create(
cache_key, cache_key,
creator, creator,
should_cache_fn=self.should_cache should_cache_fn=self._should_cache
) )
return self.from_cache_data(cached_data, **identifiers) return self._from_cache_data(cached_data, **identifiers)
def invalidate(self, **identifiers): def invalidate(self, **identifiers):
""" """
@@ -128,3 +175,21 @@ class CacheHandler(Generic[T]):
except ValueError: except ValueError:
pass # Skip if cache key can't be generated from provided identifiers pass # Skip if cache key can't be generated from provided identifiers
def invalidate_region(self):
"""
Invalidate all cache entries within this region.
Deletes all keys that start with the region prefix.
"""
# Construct the pattern for all keys in this region
pattern = f"{self.region}_{self.prefix}:*"
# Assuming Redis backend with dogpile, use `delete_multi` or direct Redis access
if hasattr(self.region.backend, 'client'):
redis_client = self.region.backend.client
keys_to_delete = redis_client.keys(pattern)
if keys_to_delete:
redis_client.delete(*keys_to_delete)
else:
# Fallback for other backends
raise NotImplementedError("Region invalidation is only supported for Redis backend.")

View File

@@ -5,11 +5,15 @@ from packaging import version
import os import os
from flask import current_app from flask import current_app
from common.utils.cache.base import CacheHandler from common.utils.cache.base import CacheHandler, CacheKey
from config.type_defs import agent_types, task_types, tool_types, specialist_types from config.type_defs import agent_types, task_types, tool_types, specialist_types
def is_major_minor(version: str) -> bool:
parts = version.strip('.').split('.')
return len(parts) == 2 and all(part.isdigit() for part in parts)
class BaseConfigCacheHandler(CacheHandler[Dict[str, Any]]): class BaseConfigCacheHandler(CacheHandler[Dict[str, Any]]):
"""Base handler for configuration caching""" """Base handler for configuration caching"""
@@ -23,18 +27,111 @@ class BaseConfigCacheHandler(CacheHandler[Dict[str, Any]]):
self.config_type = config_type self.config_type = config_type
self._types_module = None # Set by subclasses self._types_module = None # Set by subclasses
self._config_dir = None # Set by subclasses self._config_dir = None # Set by subclasses
self.version_tree_cache = None
self.configure_keys('type_name', 'version')
def configure_keys_for_operation(self, operation: str): def _to_cache_data(self, instance: Dict[str, Any]) -> Dict[str, Any]:
"""Configure required keys based on operation""" """Convert the data to a cacheable format"""
match operation: # For configuration data, we can just return the dictionary as is
case 'get_types': # since it's already in a serializable format
self.configure_keys('type_name') # Only require type_name for type definitions return instance
case 'get_versions':
self.configure_keys('type_name') # Only type_name needed for version tree def _from_cache_data(self, data: Dict[str, Any], **kwargs) -> Dict[str, Any]:
case 'get_config': """Convert cached data back to usable format"""
self.configure_keys('type_name', 'version') # Both needed for specific config # Similarly, we can return the data directly since it's already
case _: # in the format we need
raise ValueError(f"Unknown operation: {operation}") return data
def _should_cache(self, value: Dict[str, Any]) -> bool:
"""
Validate if the value should be cached
Args:
value: The value to be cached
Returns:
bool: True if the value should be cached
"""
return isinstance(value, dict) # Cache all dictionaries
def set_version_tree_cache(self, cache):
"""Set the version tree cache dependency."""
self.version_tree_cache = cache
def _load_specific_config(self, type_name: str, version_str: str) -> Dict[str, Any]:
"""
Load a specific configuration version
Args:
type_name: Type name
version_str: Version string
Returns:
Configuration data
"""
current_app.logger.debug(f"Loading specific configuration for {type_name}, version: {version_str} - no cache")
version_tree = self.version_tree_cache.get_versions(type_name)
versions = version_tree['versions']
if version_str == 'latest':
version_str = version_tree['latest_version']
if version_str not in versions:
raise ValueError(f"Version {version_str} not found for {type_name}")
file_path = versions[version_str]['file_path']
try:
with open(file_path) as f:
config = yaml.safe_load(f)
current_app.logger.debug(f"Loaded config for {type_name}{version_str}: {config}")
return config
except Exception as e:
raise ValueError(f"Error loading config from {file_path}: {e}")
def get_config(self, type_name: str, version: Optional[str] = None) -> Dict[str, Any]:
"""
Get configuration for a specific type and version
If version not specified, returns latest
Args:
type_name: Configuration type name
version: Optional specific version to retrieve
Returns:
Configuration data
"""
current_app.logger.debug(f"Trying to retrieve config for {self.config_type}, type name: {type_name}, "
f"version: {version}")
if version is None:
version_str = self.version_tree_cache.get_latest_version(type_name)
elif is_major_minor(version):
version_str = self.version_tree_cache.get_latest_patch_version(type_name, version)
else:
version_str = version
result = self.get(
lambda type_name, version: self._load_specific_config(type_name, version),
type_name=type_name,
version=version_str
)
return result
class BaseConfigVersionTreeCacheHandler(CacheHandler[Dict[str, Any]]):
"""Base handler for configuration version tree caching"""
def __init__(self, region, config_type: str):
"""
Args:
region: Cache region
config_type: Type of configuration (agents, tasks, etc.)
"""
super().__init__(region, f'config_{config_type}_version_tree')
self.config_type = config_type
self._types_module = None # Set by subclasses
self._config_dir = None # Set by subclasses
self.configure_keys('type_name')
def _load_version_tree(self, type_name: str) -> Dict[str, Any]: def _load_version_tree(self, type_name: str) -> Dict[str, Any]:
""" """
@@ -46,6 +143,7 @@ class BaseConfigCacheHandler(CacheHandler[Dict[str, Any]]):
Returns: Returns:
Dict containing available versions and their metadata Dict containing available versions and their metadata
""" """
current_app.logger.debug(f"Loading version tree for {type_name} - no cache")
type_path = Path(self._config_dir) / type_name type_path = Path(self._config_dir) / type_name
if not type_path.exists(): if not type_path.exists():
raise ValueError(f"No configuration found for type {type_name}") raise ValueError(f"No configuration found for type {type_name}")
@@ -81,25 +179,25 @@ class BaseConfigCacheHandler(CacheHandler[Dict[str, Any]]):
continue continue
current_app.logger.debug(f"Loaded versions for {type_name}: {versions}") current_app.logger.debug(f"Loaded versions for {type_name}: {versions}")
current_app.logger.debug(f"Loaded versions for {type_name}: {latest_version}") current_app.logger.debug(f"Latest version for {type_name}: {latest_version}")
return { return {
'versions': versions, 'versions': versions,
'latest_version': latest_version 'latest_version': latest_version
} }
def to_cache_data(self, instance: Dict[str, Any]) -> Dict[str, Any]: def _to_cache_data(self, instance: Dict[str, Any]) -> Dict[str, Any]:
"""Convert the data to a cacheable format""" """Convert the data to a cacheable format"""
# For configuration data, we can just return the dictionary as is # For configuration data, we can just return the dictionary as is
# since it's already in a serializable format # since it's already in a serializable format
return instance return instance
def from_cache_data(self, data: Dict[str, Any], **kwargs) -> Dict[str, Any]: def _from_cache_data(self, data: Dict[str, Any], **kwargs) -> Dict[str, Any]:
"""Convert cached data back to usable format""" """Convert cached data back to usable format"""
# Similarly, we can return the data directly since it's already # Similarly, we can return the data directly since it's already
# in the format we need # in the format we need
return data return data
def should_cache(self, value: Dict[str, Any]) -> bool: def _should_cache(self, value: Dict[str, Any]) -> bool:
""" """
Validate if the value should be cached Validate if the value should be cached
@@ -109,65 +207,7 @@ class BaseConfigCacheHandler(CacheHandler[Dict[str, Any]]):
Returns: Returns:
bool: True if the value should be cached bool: True if the value should be cached
""" """
if not isinstance(value, dict): return isinstance(value, dict) # Cache all dictionaries
return False
# For type definitions
if 'name' in value and 'description' in value:
return True
# For configurations
if 'versions' in value and 'latest_version' in value:
return True
return False
def _load_type_definitions(self) -> Dict[str, Dict[str, str]]:
"""Load type definitions from the corresponding type_defs module"""
if not self._types_module:
raise ValueError("_types_module must be set by subclass")
type_definitions = {
type_id: {
'name': info['name'],
'description': info['description']
}
for type_id, info in self._types_module.items()
}
current_app.logger.debug(f"Loaded type definitions: {type_definitions}")
return type_definitions
def _load_specific_config(self, type_name: str, version_str: str) -> Dict[str, Any]:
"""
Load a specific configuration version
Args:
type_name: Type name
version_str: Version string
Returns:
Configuration data
"""
version_tree = self.get_versions(type_name)
versions = version_tree['versions']
if version_str == 'latest':
version_str = version_tree['latest_version']
if version_str not in versions:
raise ValueError(f"Version {version_str} not found for {type_name}")
file_path = versions[version_str]['file_path']
try:
with open(file_path) as f:
config = yaml.safe_load(f)
current_app.logger.debug(f"Loaded config for {type_name}{version_str}: {config}")
return config
except Exception as e:
raise ValueError(f"Error loading config from {file_path}: {e}")
def get_versions(self, type_name: str) -> Dict[str, Any]: def get_versions(self, type_name: str) -> Dict[str, Any]:
""" """
@@ -179,7 +219,7 @@ class BaseConfigCacheHandler(CacheHandler[Dict[str, Any]]):
Returns: Returns:
Dict with version information Dict with version information
""" """
self.configure_keys_for_operation('get_versions') current_app.logger.debug(f"Trying to get version tree for {self.config_type}, {type_name}")
return self.get( return self.get(
lambda type_name: self._load_version_tree(type_name), lambda type_name: self._load_version_tree(type_name),
type_name=type_name type_name=type_name
@@ -235,72 +275,146 @@ class BaseConfigCacheHandler(CacheHandler[Dict[str, Any]]):
latest_patch = max(matching_versions, key=version.parse) latest_patch = max(matching_versions, key=version.parse)
return latest_patch return latest_patch
class BaseConfigTypesCacheHandler(CacheHandler[Dict[str, Any]]):
"""Base handler for configuration types caching"""
def __init__(self, region, config_type: str):
"""
Args:
region: Cache region
config_type: Type of configuration (agents, tasks, etc.)
"""
super().__init__(region, f'config_{config_type}_types')
self.config_type = config_type
self._types_module = None # Set by subclasses
self._config_dir = None # Set by subclasses
self.configure_keys()
def _to_cache_data(self, instance: Dict[str, Any]) -> Dict[str, Any]:
"""Convert the data to a cacheable format"""
# For configuration data, we can just return the dictionary as is
# since it's already in a serializable format
return instance
def _from_cache_data(self, data: Dict[str, Any], **kwargs) -> Dict[str, Any]:
"""Convert cached data back to usable format"""
# Similarly, we can return the data directly since it's already
# in the format we need
return data
def _should_cache(self, value: Dict[str, Any]) -> bool:
"""
Validate if the value should be cached
Args:
value: The value to be cached
Returns:
bool: True if the value should be cached
"""
return isinstance(value, dict) # Cache all dictionaries
def _load_type_definitions(self) -> Dict[str, Dict[str, str]]:
"""Load type definitions from the corresponding type_defs module"""
current_app.logger.debug(f"Loading type definitions for {self.config_type} - no cache")
if not self._types_module:
raise ValueError("_types_module must be set by subclass")
type_definitions = {
type_id: {
'name': info['name'],
'description': info['description']
}
for type_id, info in self._types_module.items()
}
current_app.logger.debug(f"Loaded type definitions: {type_definitions}")
return type_definitions
def get_types(self) -> Dict[str, Dict[str, str]]: def get_types(self) -> Dict[str, Dict[str, str]]:
"""Get dictionary of available types with name and description""" """Get dictionary of available types with name and description"""
self.configure_keys_for_operation('get_types') current_app.logger.debug(f"Trying to retrieve type definitions for {self.config_type}")
result = self.get( result = self.get(
lambda type_name: self._load_type_definitions(), lambda type_name: self._load_type_definitions(),
type_name=f'{self.config_type}_types', type_name=f'{self.config_type}_types',
) )
return result return result
def get_config(self, type_name: str, version: Optional[str] = None) -> Dict[str, Any]:
"""
Get configuration for a specific type and version
If version not specified, returns latest
Args: def create_config_cache_handlers(config_type: str, config_dir: str, types_module: dict) -> tuple:
type_name: Configuration type name """
version: Optional specific version to retrieve Factory function to dynamically create the 3 cache handler classes for a given configuration type.
The following cache names are created:
Returns: - <config_type>_config_cache
Configuration data - <config_type>_version_tree_cache
""" - <config_type>_types_cache
self.configure_keys_for_operation('get_config')
version_str = version or 'latest'
return self.get(
lambda type_name, version: self._load_specific_config(type_name, version),
type_name=type_name,
version=version_str
)
class AgentConfigCacheHandler(BaseConfigCacheHandler): Args:
"""Handler for agent configurations""" config_type: The configuration type (e.g., 'agents', 'tasks').
handler_name = 'agent_config_cache' config_dir: The directory where configuration files are stored.
types_module: The types module defining the available types for this config.
def __init__(self, region): Returns:
super().__init__(region, 'agents') A tuple of dynamically created classes for config, version tree, and types handlers.
self._types_module = agent_types.AGENT_TYPES """
self._config_dir = os.path.join('config', 'agents')
class ConfigCacheHandler(BaseConfigCacheHandler):
handler_name = f"{config_type}_config_cache"
def __init__(self, region):
super().__init__(region, config_type)
self._types_module = types_module
self._config_dir = config_dir
class VersionTreeCacheHandler(BaseConfigVersionTreeCacheHandler):
handler_name = f"{config_type}_version_tree_cache"
def __init__(self, region):
super().__init__(region, config_type)
self._types_module = types_module
self._config_dir = config_dir
class TypesCacheHandler(BaseConfigTypesCacheHandler):
handler_name = f"{config_type}_types_cache"
def __init__(self, region):
super().__init__(region, config_type)
self._types_module = types_module
self._config_dir = config_dir
return ConfigCacheHandler, VersionTreeCacheHandler, TypesCacheHandler
class TaskConfigCacheHandler(BaseConfigCacheHandler): AgentConfigCacheHandler, AgentConfigVersionTreeCacheHandler, AgentConfigTypesCacheHandler = (
"""Handler for task configurations""" create_config_cache_handlers(
handler_name = 'task_config_cache' config_type='agents',
config_dir='config/agents',
def __init__(self, region): types_module=agent_types.AGENT_TYPES
super().__init__(region, 'tasks') ))
self._types_module = task_types.TASK_TYPES
self._config_dir = os.path.join('config', 'tasks')
class ToolConfigCacheHandler(BaseConfigCacheHandler): TaskConfigCacheHandler, TaskConfigVersionTreeCacheHandler, TaskConfigTypesCacheHandler = (
"""Handler for tool configurations""" create_config_cache_handlers(
handler_name = 'tool_config_cache' config_type='tasks',
config_dir='config/tasks',
def __init__(self, region): types_module=task_types.TASK_TYPES
super().__init__(region, 'tools') ))
self._types_module = tool_types.TOOL_TYPES
self._config_dir = os.path.join('config', 'tools')
class SpecialistConfigCacheHandler(BaseConfigCacheHandler): ToolConfigCacheHandler, ToolConfigVersionTreeCacheHandler, ToolConfigTypesCacheHandler = (
"""Handler for specialist configurations""" create_config_cache_handlers(
handler_name = 'specialist_config_cache' config_type='tools',
config_dir='config/tools',
types_module=tool_types.TOOL_TYPES
))
def __init__(self, region):
super().__init__(region, 'specialists') SpecialistConfigCacheHandler, SpecialistConfigVersionTreeCacheHandler, SpecialistConfigTypesCacheHandler = (
self._types_module = specialist_types.SPECIALIST_TYPES create_config_cache_handlers(
self._config_dir = os.path.join('config', 'specialists') config_type='specialists',
config_dir='config/specialists',
types_module=specialist_types.SPECIALIST_TYPES
))

View File

@@ -65,11 +65,7 @@ def create_cache_regions(app):
eveai_config_region = make_region(name='eveai_config').configure( eveai_config_region = make_region(name='eveai_config').configure(
'dogpile.cache.redis', 'dogpile.cache.redis',
arguments={ arguments=redis_config,
**redis_config,
'redis_expiration_time': None, # No expiration in Redis
'key_mangler': lambda key: f"startup_{startup_time}:{key}" # Prefix all keys
},
replace_existing_backend=True replace_existing_backend=True
) )
regions['eveai_config'] = eveai_config_region regions['eveai_config'] = eveai_config_region

View File

@@ -23,7 +23,7 @@ def initialize_specialist(specialist_id: int, specialist_type: str, specialist_v
ValueError: If specialist not found or invalid configuration ValueError: If specialist not found or invalid configuration
SQLAlchemyError: If database operations fail SQLAlchemyError: If database operations fail
""" """
config = cache_manager.specialist_config_cache.get_config(specialist_type, specialist_version) config = cache_manager.specialists_config_cache.get_config(specialist_type, specialist_version)
if not config: if not config:
raise ValueError(f"No configuration found for {specialist_type} version {specialist_version}") raise ValueError(f"No configuration found for {specialist_type} version {specialist_version}")
if config['framework'] == 'langchain': if config['framework'] == 'langchain':
@@ -99,16 +99,18 @@ def _create_agent(
timestamp: Optional[dt] = None timestamp: Optional[dt] = None
) -> EveAIAgent: ) -> EveAIAgent:
"""Create an agent with the given configuration.""" """Create an agent with the given configuration."""
current_app.logger.debug(f"Creating agent {agent_type} {agent_version} with {name}, {description}")
if timestamp is None: if timestamp is None:
timestamp = dt.now(tz=tz.utc) timestamp = dt.now(tz=tz.utc)
# Get agent configuration from cache # Get agent configuration from cache
agent_config = cache_manager.agent_config_cache.get_config(agent_type, agent_version) agent_config = cache_manager.agents_config_cache.get_config(agent_type, agent_version)
current_app.logger.debug(f"Agent Config: {agent_config}")
agent = EveAIAgent( agent = EveAIAgent(
specialist_id=specialist_id, specialist_id=specialist_id,
name=name or agent_config.get('name', agent_type), name=name or agent_config.get('name', agent_type),
description=description or agent_config.get('description', ''), description=description or agent_config.get('metadata').get('description', ''),
type=agent_type, type=agent_type,
type_version=agent_version, type_version=agent_version,
role=None, role=None,
@@ -122,6 +124,7 @@ def _create_agent(
set_logging_information(agent, timestamp) set_logging_information(agent, timestamp)
db.session.add(agent) db.session.add(agent)
current_app.logger.info(f"Created agent {agent.id} of type {agent_type}")
return agent return agent
@@ -138,14 +141,16 @@ def _create_task(
timestamp = dt.now(tz=tz.utc) timestamp = dt.now(tz=tz.utc)
# Get task configuration from cache # Get task configuration from cache
task_config = cache_manager.task_config_cache.get_config(task_type, task_version) task_config = cache_manager.tasks_config_cache.get_config(task_type, task_version)
current_app.logger.debug(f"Task Config: {task_config}")
task = EveAITask( task = EveAITask(
specialist_id=specialist_id, specialist_id=specialist_id,
name=name or task_config.get('name', task_type), name=name or task_config.get('name', task_type),
description=description or task_config.get('description', ''), description=description or task_config.get('metadata').get('description', ''),
type=task_type, type=task_type,
type_version=task_version, type_version=task_version,
task_description=None,
expected_output=None, expected_output=None,
tuning=False, tuning=False,
configuration=None, configuration=None,
@@ -157,6 +162,7 @@ def _create_task(
set_logging_information(task, timestamp) set_logging_information(task, timestamp)
db.session.add(task) db.session.add(task)
current_app.logger.info(f"Created task {task.id} of type {task_type}")
return task return task
@@ -173,12 +179,13 @@ def _create_tool(
timestamp = dt.now(tz=tz.utc) timestamp = dt.now(tz=tz.utc)
# Get tool configuration from cache # Get tool configuration from cache
tool_config = cache_manager.tool_config_cache.get_config(tool_type, tool_version) tool_config = cache_manager.tools_config_cache.get_config(tool_type, tool_version)
current_app.logger.debug(f"Tool Config: {tool_config}")
tool = EveAITool( tool = EveAITool(
specialist_id=specialist_id, specialist_id=specialist_id,
name=name or tool_config.get('name', tool_type), name=name or tool_config.get('name', tool_type),
description=description or tool_config.get('description', ''), description=description or tool_config.get('metadata').get('description', ''),
type=tool_type, type=tool_type,
type_version=tool_version, type_version=tool_version,
tuning=False, tuning=False,
@@ -189,4 +196,5 @@ def _create_tool(
set_logging_information(tool, timestamp) set_logging_information(tool, timestamp)
db.session.add(tool) db.session.add(tool)
current_app.logger.info(f"Created tool {tool.id} of type {tool_type}")
return tool return tool

View File

@@ -28,10 +28,15 @@ def perform_startup_invalidation(app):
try: try:
# Check if invalidation was already performed # Check if invalidation was already performed
if not redis_client.get(marker_key): if not redis_client.get(marker_key):
app.logger.debug(f"Performing cache invalidation at startup time {startup_time}")
app.logger.debug(f"Current cache keys: {redis_client.keys('*')}")
# Perform invalidation # Perform invalidation
cache_manager.invalidate_region('eveai_config') cache_manager.invalidate_region('eveai_config')
# Set marker with 1 hour expiry (longer than any reasonable startup sequence)
redis_client.setex(marker_key, 300, str(startup_time)) app.logger.debug(f"Cache keys after invalidation: {redis_client.keys('*')}")
redis_client.setex(marker_key, 180, str(startup_time))
app.logger.info("Startup cache invalidation completed") app.logger.info("Startup cache invalidation completed")
else: else:
app.logger.info("Startup cache invalidation already performed") app.logger.info("Startup cache invalidation already performed")

View File

@@ -166,7 +166,7 @@ class DevConfig(Config):
DEVELOPMENT = True DEVELOPMENT = True
DEBUG = True DEBUG = True
FLASK_DEBUG = True FLASK_DEBUG = True
EXPLAIN_TEMPLATE_LOADING = True EXPLAIN_TEMPLATE_LOADING = False
# Database Settings # Database Settings
DB_HOST = environ.get('DB_HOST', 'localhost') DB_HOST = environ.get('DB_HOST', 'localhost')

View File

@@ -89,37 +89,38 @@ results:
type: "List[str]" type: "List[str]"
description: "A list of needs" description: "A list of needs"
required: false required: false
agents: agents:
- type: "RAG_AGENT" - type: "RAG_AGENT"
version: 1.0 version: "1.0"
name: "Default RAG Agent" # Just added as an example. Overwrites the default agent name. name: "Default RAG Agent" # Just added as an example. Overwrites the default agent name.
description: "An Agent that does RAG based on a user's question, RAG content & history" # Just added as an example. Overwrites the default agent description. description: "An Agent that does RAG based on a user's question, RAG content & history" # Just added as an example. Overwrites the default agent description.
- type: "SPIN_DETECTION_AGENT" - type: "SPIN_DETECTION_AGENT"
version: 1.0 version: "1.0"
- type: "SPIN_SALES_SPECIALIST_AGENT" - type: "SPIN_SALES_SPECIALIST_AGENT"
version: 1.0 version: "1.0"
- type: "IDENTIFICATION_AGENT" - type: "IDENTIFICATION_AGENT"
version: 1.0 version: "1.0"
- type: "EMAIL_CONTENT_AGENT" - type: "EMAIL_CONTENT_AGENT"
version: 1.0 version: "1.0"
- type: "EMAIL_LEAD_ENGAGEMENT" - type: "EMAIL_ENGAGEMENT_AGENT"
version: 1.0 version: "1.0"
tasks: tasks:
- type: "RAG_TASK" - type: "RAG_TASK"
version: 1.0 version: "1.0"
- type: "SPIN_DETECT_TASK" - type: "SPIN_DETECT_TASK"
version: 1.0 version: "1.0"
- type: "SPIN_QUESTIONS_TASK" - type: "SPIN_QUESTIONS_TASK"
version: 1.0 version: "1.0"
- type: "IDENTIFICATION_DETECTION_TASK" - type: "IDENTIFICATION_DETECTION_TASK"
version: 1.0 version: "1.0"
- type: "IDENTIFICATION_QUESTIONS_TASK" - type: "IDENTIFICATION_QUESTIONS_TASK"
version: 1.0 version: "1.0"
- type: "EMAIL_LEAD_DRAFTING" - type: "EMAIL_LEAD_DRAFTING_TASK"
version: 1.0 version: "1.0"
- type: "EMAIL_LEAD_ENGAGEMENT" - type: "EMAIL_LEAD_ENGAGEMENT_TASK"
version: 1.0 version: "1.0"
metadata: metadata:
author: "Josako" author: "Josako"
date_added: "2025-01-08" date_added: "2025-01-08"
changes: "Initial version" changes: "Initial version"
description: "A Specialist that performs both Q&A as SPIN (Sales Process) activities"

View File

@@ -49,3 +49,4 @@ metadata:
author: "Josako" author: "Josako"
date_added: "2025-01-08" date_added: "2025-01-08"
changes: "Initial version" changes: "Initial version"
description: "A Specialist that performs standard Q&A"

View File

@@ -1,6 +1,6 @@
version: "1.0.0" version: "1.0.0"
name: "Email Lead Draft Creation" name: "Email Lead Draft Creation"
description: > task_description: >
Craft a highly personalized email using the lead's name, job title, company information, and any relevant personal or Craft a highly personalized email using the lead's name, job title, company information, and any relevant personal or
company achievements when available. The email should speak directly to the lead's interests and the needs company achievements when available. The email should speak directly to the lead's interests and the needs
of their company. of their company.

View File

@@ -1,6 +1,6 @@
version: "1.0.0" version: "1.0.0"
name: "Email Lead Engagement Creation" name: "Email Lead Engagement Creation"
description: > task_description: >
Review a personalized email and optimize it with strong CTAs and engagement hooks. Keep in mind that this email is Review a personalized email and optimize it with strong CTAs and engagement hooks. Keep in mind that this email is
the consequence of a first conversation. the consequence of a first conversation.
Don't use any salutations or closing remarks, nor too complex sentences. Keep it short and to the point. Don't use any salutations or closing remarks, nor too complex sentences. Keep it short and to the point.

View File

@@ -1,6 +1,6 @@
version: "1.0.0" version: "1.0.0"
name: "Identification Gathering" name: "Identification Gathering"
description: > task_description: >
Detect and pass on identification information in the ongoing conversation, from within the following information: Detect and pass on identification information in the ongoing conversation, from within the following information:
{question} {question}
Add to or refine the following already gathered identification information (between triple $) Add to or refine the following already gathered identification information (between triple $)

View File

@@ -1,6 +1,6 @@
version: "1.0.0" version: "1.0.0"
name: "Define Identification Questions" name: "Define Identification Questions"
description: > task_description: >
Ask questions to complete or confirm the identification information gathered. Ask questions to complete or confirm the identification information gathered.
Current Identification Information: Current Identification Information:
$$${Identification}$$$ $$${Identification}$$$

View File

@@ -1,6 +1,6 @@
version: "1.0.0" version: "1.0.0"
name: "RAG Task" name: "RAG Task"
description: > task_description: >
Answer the question based on the following context, delimited between triple backquotes, and taking into account Answer the question based on the following context, delimited between triple backquotes, and taking into account
the history of the discussion, in between triple % the history of the discussion, in between triple %
{custom_description} {custom_description}

View File

@@ -1,6 +1,6 @@
version: "1.0.0" version: "1.0.0"
name: "SPIN Information Detection" name: "SPIN Information Detection"
description: > task_description: >
Detect the SPIN-context, taking into account the history of the discussion (in between triple %) with main focus on Detect the SPIN-context, taking into account the history of the discussion (in between triple %) with main focus on
the latest reply (which can contain answers on previously asked questions by the user). Do not remove elements from the latest reply (which can contain answers on previously asked questions by the user). Do not remove elements from
the known SPIN (in between triple $) analysis unless explicitly stated by the end user in the latest reply. In all other cases, refine the the known SPIN (in between triple $) analysis unless explicitly stated by the end user in the latest reply. In all other cases, refine the

View File

@@ -1,6 +1,6 @@
version: "1.0.0" version: "1.0.0"
name: "SPIN Question Identification" name: "SPIN Question Identification"
description: > task_description: >
Define, taking into account the history of the discussion (in between triple %), the latest reply and the currently Define, taking into account the history of the discussion (in between triple %), the latest reply and the currently
known SPIN-elements (in between triple $), the top questions that need to be asked to understand the full SPIN context known SPIN-elements (in between triple $), the top questions that need to be asked to understand the full SPIN context
of the customer. If you think this user could be a potential customer, please indicate so. of the customer. If you think this user could be a potential customer, please indicate so.

View File

@@ -158,6 +158,9 @@ docker buildx use eveai_builder
# Loop through services # Loop through services
for SERVICE in "${SERVICES[@]}"; do for SERVICE in "${SERVICES[@]}"; do
if [[ "$SERVICE" == "nginx" ]]; then
./copy_specialist_svgs.sh ../config ../nginx/static/assets
fi
if [[ "$SERVICE" == "nginx" || "$SERVICE" == eveai_* || "$SERVICE" == "flower" ]]; then if [[ "$SERVICE" == "nginx" || "$SERVICE" == eveai_* || "$SERVICE" == "flower" ]]; then
if process_service "$SERVICE"; then if process_service "$SERVICE"; then
echo "Successfully processed $SERVICE" echo "Successfully processed $SERVICE"

60
docker/copy_specialist_svgs.sh Executable file
View File

@@ -0,0 +1,60 @@
#!/bin/bash
# Script to copy specialist overview SVGs to nginx static directory
# Function to show usage
show_usage() {
echo "Usage: $0 <config_dir> <static_dir>"
echo " config_dir: Path to the config directory containing specialists"
echo " static_dir: Path to the nginx static directory"
exit 1
}
# Check arguments
if [ $# -ne 2 ]; then
show_usage
fi
CONFIG_DIR="$1"
STATIC_DIR="$2"
SPECIALISTS_DIR="${CONFIG_DIR}/specialists"
OUTPUT_DIR="${STATIC_DIR}/specialists"
# Check if source directory exists
if [ ! -d "$SPECIALISTS_DIR" ]; then
echo "Error: Specialists directory not found at $SPECIALISTS_DIR"
exit 1
fi
# Create output directory
mkdir -p "$OUTPUT_DIR"
# Counter for processed files
processed=0
# Process each specialist type directory
for TYPE_DIR in "$SPECIALISTS_DIR"/*/ ; do
if [ -d "$TYPE_DIR" ]; then
# Get specialist type from directory name
SPECIALIST_TYPE=$(basename "$TYPE_DIR")
# Find and process overview SVG files
for SVG_FILE in "$TYPE_DIR"*_overview.svg; do
if [ -f "$SVG_FILE" ]; then
# Extract version (remove _overview.svg from filename)
VERSION=$(basename "$SVG_FILE" "_overview.svg")
# Create new filename
NEW_FILENAME="${SPECIALIST_TYPE}_${VERSION}_overview.svg"
# Copy file
cp -f "$SVG_FILE" "${OUTPUT_DIR}/${NEW_FILENAME}"
echo "Copied $(basename "$SVG_FILE") -> $NEW_FILENAME"
((processed++))
fi
done
fi
done
echo -e "\nProcessed $processed overview SVG files"

View File

@@ -9,11 +9,14 @@ COPY ../../nginx/mime.types /etc/nginx/mime.types
# Copy static & public files # Copy static & public files
RUN mkdir -p /etc/nginx/static /etc/nginx/public RUN mkdir -p /etc/nginx/static /etc/nginx/public
COPY ../../nginx/static /etc/nginx/static COPY ../../nginx/static /etc/nginx/static
COPY ../../integrations/Wordpress/eveai-chat/assets/css/eveai-chat-style.css /etc/nginx/static/css/ COPY ../../integrations/Wordpress/eveai-chat/assets/css/eveai-chat-style.css /etc/nginx/static/css/
COPY ../../integrations/Wordpress/eveai-chat/assets/js/eveai-chat-widget.js /etc/nginx/static/js/ COPY ../../integrations/Wordpress/eveai-chat/assets/js/eveai-chat-widget.js /etc/nginx/static/js/
COPY ../../integrations/Wordpress/eveai-chat/assets/js/eveai-token-manager.js /etc/nginx/static/js/ COPY ../../integrations/Wordpress/eveai-chat/assets/js/eveai-token-manager.js /etc/nginx/static/js/
COPY ../../integrations/Wordpress/eveai-chat/assets/js/eveai-sdk.js /etc/nginx/static/js COPY ../../integrations/Wordpress/eveai-chat/assets/js/eveai-sdk.js /etc/nginx/static/js
# Copy public files
COPY ../../nginx/public /etc/nginx/public COPY ../../nginx/public /etc/nginx/public
# Copy site-specific configurations # Copy site-specific configurations

View File

@@ -161,10 +161,31 @@ def register_blueprints(app):
def register_cache_handlers(app): def register_cache_handlers(app):
from common.utils.cache.config_cache import ( from common.utils.cache.config_cache import (
AgentConfigCacheHandler, TaskConfigCacheHandler, ToolConfigCacheHandler, SpecialistConfigCacheHandler) AgentConfigCacheHandler, AgentConfigTypesCacheHandler, AgentConfigVersionTreeCacheHandler,
TaskConfigCacheHandler, TaskConfigTypesCacheHandler, TaskConfigVersionTreeCacheHandler,
ToolConfigCacheHandler, ToolConfigTypesCacheHandler, ToolConfigVersionTreeCacheHandler,
SpecialistConfigCacheHandler, SpecialistConfigTypesCacheHandler, SpecialistConfigVersionTreeCacheHandler,)
cache_manager.register_handler(AgentConfigCacheHandler, 'eveai_config') cache_manager.register_handler(AgentConfigCacheHandler, 'eveai_config')
cache_manager.register_handler(AgentConfigTypesCacheHandler, 'eveai_config')
cache_manager.register_handler(AgentConfigVersionTreeCacheHandler, 'eveai_config')
cache_manager.register_handler(TaskConfigCacheHandler, 'eveai_config') cache_manager.register_handler(TaskConfigCacheHandler, 'eveai_config')
cache_manager.register_handler(TaskConfigTypesCacheHandler, 'eveai_config')
cache_manager.register_handler(TaskConfigVersionTreeCacheHandler, 'eveai_config')
cache_manager.register_handler(ToolConfigCacheHandler, 'eveai_config') cache_manager.register_handler(ToolConfigCacheHandler, 'eveai_config')
cache_manager.register_handler(ToolConfigTypesCacheHandler, 'eveai_config')
cache_manager.register_handler(ToolConfigVersionTreeCacheHandler, 'eveai_config')
cache_manager.register_handler(SpecialistConfigCacheHandler, 'eveai_config') cache_manager.register_handler(SpecialistConfigCacheHandler, 'eveai_config')
cache_manager.register_handler(SpecialistConfigTypesCacheHandler, 'eveai_config')
cache_manager.register_handler(SpecialistConfigVersionTreeCacheHandler, 'eveai_config')
cache_manager.agents_config_cache.set_version_tree_cache(cache_manager.agents_version_tree_cache)
cache_manager.tasks_config_cache.set_version_tree_cache(cache_manager.tasks_version_tree_cache)
cache_manager.tools_config_cache.set_version_tree_cache(cache_manager.tools_version_tree_cache)
cache_manager.specialists_config_cache.set_version_tree_cache(cache_manager.specialists_version_tree_cache)

View File

@@ -1,31 +1,28 @@
{% extends 'base.html' %}
{% from "macros.html" import render_field %} {% from "macros.html" import render_field %}
{% block title %}{{ title }}{% endblock %}
{% block content_title %}{{ title }}{% endblock %} {% block content_title %}{{ title }}{% endblock %}
{% block content_description %}{{ description }}{% endblock %} {% block content_description %}{{ description }}{% endblock %}
{% block content %} {% block content %}
<form method="post"> {% set disabled_fields = [] %}
{{ form.hidden_tag() }} {% set exclude_fields = [] %}
{% set disabled_fields = [] %} {% for field in form.get_static_fields() %}
{% set exclude_fields = [] %} {{ render_field(field, disabled_fields, exclude_fields) }}
{% for field in form.get_static_fields() %} {% endfor %}
{{ render_field(field, disabled_fields, exclude_fields) }} {% if form.get_dynamic_fields is defined %}
{% endfor %} {% for collection_name, fields in form.get_dynamic_fields().items() %}
{% if form.get_dynamic_fields is defined %} {% if fields|length > 0 %}
{% for collection_name, fields in form.get_dynamic_fields().items() %} <h4 class="mt-4">{{ collection_name }}</h4>
{% if fields|length > 0 %} {% endif %}
<h4 class="mt-4">{{ collection_name }}</h4> {% for field in fields %}
{% endif %} {{ render_field(field, disabled_fields, exclude_fields) }}
{% for field in fields %}
{{ render_field(field, disabled_fields, exclude_fields) }}
{% endfor %}
{% endfor %} {% endfor %}
{% endif %} {% endfor %}
<button type="submit" class="btn btn-primary">{{ submit_text }}</button> {% endif %}
</form> <div class="btn-group mt-3">
<button type="submit" class="btn btn-primary component-submit">{{ submit_text }}</button>
<button type="button" class="btn btn-secondary ms-2" id="cancelEdit">Cancel</button>
</div>
{% endblock %} {% endblock %}
{% block content_footer %} {% block content_footer %}

View File

@@ -0,0 +1 @@
{% extends "interaction/component.html" %}

View File

@@ -1,33 +1,479 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% from "macros.html" import render_field %} {% from "macros.html" import render_field, render_selectable_table %}
{% block title %}Edit Specialist{% endblock %} {% block title %}Edit Specialist{% endblock %}
{% block content_title %}Edit Specialist{% endblock %} {% block content_title %}Edit Specialist{% endblock %}
{% block content_description %}Edit a Specialist{% endblock %} {% block content_description %}Edit a Specialist and its components{% endblock %}
{% block content %} {% block content %}
<form method="post"> <div class="container-fluid px-0">
{{ form.hidden_tag() }} <div class="row">
{% set disabled_fields = ['type'] %} <!-- Main Specialist Editor -->
{% set exclude_fields = [] %} <div class="col-12" id="mainEditorSection">
<!-- Render Static Fields --> <form method="post" id="specialistForm" action="{{ url_for('interaction_bp.edit_specialist', specialist_id=specialist_id) }}">
{% for field in form.get_static_fields() %} {{ form.hidden_tag() }}
{{ render_field(field, disabled_fields, exclude_fields) }} {% set disabled_fields = ['type', 'type_version'] %}
{% endfor %} {% set exclude_fields = [] %}
<!-- Render Dynamic Fields --> <!-- Render Static Fields -->
{% for collection_name, fields in form.get_dynamic_fields().items() %} {% for field in form.get_static_fields() %}
{% if fields|length > 0 %} {{ render_field(field, disabled_fields, exclude_fields) }}
<h4 class="mt-4">{{ collection_name }}</h4> {% endfor %}
{% endif %}
{% for field in fields %} <!-- Overview Section -->
{{ render_field(field, disabled_fields, exclude_fields) }} <div class="row mb-4">
{% endfor %} <div class="col-12">
{% endfor %} <div class="card">
<button type="submit" class="btn btn-primary">Save Specialist</button> <div class="card-body">
</form> <div class="specialist-overview" id="specialist-svg">
<img src="{{ svg_path }}" alt="Specialist Overview" class="w-100">
</div>
</div>
</div>
</div>
</div>
<!-- Nav Tabs -->
<div class="row mt-5">
<div class="col-lg-12">
<div class="nav-wrapper position-relative end-0">
<ul class="nav nav-pills nav-fill p-1" role="tablist">
<li class="nav-item">
<a class="nav-link mb-0 px-0 py-1 active" data-bs-toggle="tab" href="#configuration-tab" role="tab">
Configuration
</a>
</li>
<li class="nav-item">
<a class="nav-link mb-0 px-0 py-1" data-bs-toggle="tab" href="#agents-tab" role="tab">
Agents
</a>
</li>
<li class="nav-item">
<a class="nav-link mb-0 px-0 py-1" data-bs-toggle="tab" href="#tasks-tab" role="tab">
Tasks
</a>
</li>
<li class="nav-item">
<a class="nav-link mb-0 px-0 py-1" data-bs-toggle="tab" href="#tools-tab" role="tab">
Tools
</a>
</li>
<li class="nav-item">
<a class="nav-link mb-0 px-0 py-1 d-none" id="editor-tab-link" data-bs-toggle="tab" href="#editor-tab" role="tab">
Editor
</a>
</li>
</ul>
</div>
<div class="tab-content tab-space">
<!-- Configuration Tab -->
<div class="tab-pane fade show active" id="configuration-tab" role="tabpanel">
{% for collection_name, fields in form.get_dynamic_fields().items() %}
{% if fields|length > 0 %}
<h4 class="mt-4">{{ collection_name }}</h4>
{% endif %}
{% for field in fields %}
{{ render_field(field, disabled_fields, exclude_fields) }}
{% endfor %}
{% endfor %}
</div>
<!-- Agents Tab -->
<div class="tab-pane fade" id="agents-tab" role="tabpanel">
<div class="card">
<div class="card-body">
{{ render_selectable_table(
headers=["Agent ID", "Name", "Type", "Status"],
rows=agent_rows if agent_rows else [],
selectable=True,
id="agentsTable",
is_component_selector=True
) }}
<div class="form-group mt-3">
<button type="button" class="btn btn-primary edit-component"
data-component-type="agent"
data-edit-url="{{ prefixed_url_for('interaction_bp.edit_agent', agent_id=0) }}">Edit Agent
</button>
</div>
</div>
</div>
</div>
<!-- Tasks Tab -->
<div class="tab-pane fade" id="tasks-tab" role="tabpanel">
<div class="card">
<div class="card-body">
{{ render_selectable_table(
headers=["Task ID", "Name", "Type", "Status"],
rows=task_rows if task_rows else [],
selectable=True,
id="tasksTable",
is_component_selector=True
) }}
<div class="form-group mt-3">
<button type="button" class="btn btn-primary edit-component"
data-component-type="task"
data-edit-url="{{ prefixed_url_for('interaction_bp.edit_task', task_id=0) }}">Edit Task
</button>
</div>
</div>
</div>
</div>
<!-- Tools Tab -->
<div class="tab-pane fade" id="tools-tab" role="tabpanel">
<div class="card">
<div class="card-body">
{{ render_selectable_table(
headers=["Tool ID", "Name", "Type", "Status"],
rows=tool_rows if tool_rows else [],
selectable=True,
id="toolsTable",
is_component_selector=True
) }}
<div class="form-group mt-3">
<button type="button" class="btn btn-primary edit-component"
data-component-type="tool"
data-edit-url="{{ prefixed_url_for('interaction_bp.edit_tool', tool_id=0) }}">Edit Tool
</button>
</div>
</div>
</div>
</div>
<!-- Editor Tab -->
<div class="tab-pane fade" id="editor-tab" role="tabpanel">
<div class="card">
<div class="card-header">
<h5 class="mb-0" id="editorTitle"></h5>
</div>
<div class="card-body" id="editorContent">
<!-- Component editor will be loaded here -->
</div>
</div>
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary mt-4">Save Specialist</button>
</form>
</div>
</div>
</div>
{% endblock %} {% endblock %}
{% block content_footer %} {% block scripts %}
{{ super() }}
<script>
document.addEventListener('DOMContentLoaded', function() {
const editorTab = document.getElementById('editor-tab');
const editorTabLink = document.getElementById('editor-tab-link');
const editorTitle = document.getElementById('editorTitle');
const editorContent = document.getElementById('editorContent');
let previousTab = null;
// Add color classes to the tabs
const agentsTabLink = document.querySelector('[href="#agents-tab"]');
const tasksTabLink = document.querySelector('[href="#tasks-tab"]');
const toolsTabLink = document.querySelector('[href="#tools-tab"]');
agentsTabLink.classList.add('component-agent');
tasksTabLink.classList.add('component-task');
toolsTabLink.classList.add('component-tool');
// Add background colors to the tab panes
const agentsTab = document.getElementById('agents-tab');
const tasksTab = document.getElementById('tasks-tab');
const toolsTab = document.getElementById('tools-tab');
agentsTab.classList.add('component-agent-bg');
tasksTab.classList.add('component-task-bg');
toolsTab.classList.add('component-tool-bg');
// Ensure component selectors don't interfere with form submission
const form = document.getElementById('specialistForm');
form.addEventListener('submit', function(e) {
// Remove component selectors from form validation
const componentSelectors = form.querySelectorAll('input[data-component-selector]');
componentSelectors.forEach(selector => {
selector.removeAttribute('required');
});
});
// Get all tab links except the editor tab
const tabLinks = Array.from(document.querySelectorAll('.nav-link')).filter(link => link.id !== 'editor-tab-link');
// Function to toggle other tabs' disabled state
function toggleOtherTabs(disable) {
tabLinks.forEach(link => {
if (disable) {
link.classList.add('disabled');
} else {
link.classList.remove('disabled');
}
});
}
// Function to toggle main form elements
const mainSubmitButton = document.querySelector('#specialistForm > .btn-primary');
function toggleMainFormElements(disable) {
// Toggle tabs
document.querySelectorAll('.nav-link').forEach(link => {
if (link.id !== 'editor-tab-link') {
if (disable) {
link.classList.add('disabled');
} else {
link.classList.remove('disabled');
}
}
});
// Toggle main submit button
if (mainSubmitButton) {
mainSubmitButton.disabled = disable;
}
}
// Handle edit buttons
document.querySelectorAll('.edit-component').forEach(button => {
button.addEventListener('click', function() {
const componentType = this.dataset.componentType;
const form = this.closest('form');
const selectedRow = form.querySelector('input[type="radio"]:checked');
console.log("I'm in the custom click event listener!")
if (!selectedRow) {
alert('Please select a component to edit');
return;
}
const valueMatch = selectedRow.value.match(/'value':\s*(\d+)/);
const selectedId = valueMatch ? valueMatch[1] : null;
if (!selectedId) {
console.error('Could not extract ID from value:', selectedRow.value);
alert('Error: Could not determine component ID');
return;
}
// Make AJAX call to get component editor
const urlTemplate = this.dataset.editUrl.replace('/0', `/${selectedId}`);
fetch(urlTemplate, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.text();
})
.then(html => {
// Store the current active tab
previousTab = document.querySelector('.nav-link.active');
// Update editor content
editorTitle.textContent = `Edit ${componentType.charAt(0).toUpperCase() + componentType.slice(1)}`;
editorContent.innerHTML = html;
// Apply the appropriate color class to the editor tab
editorTabLink.classList.remove('component-agent', 'component-task', 'component-tool');
editorTab.classList.remove('component-agent-bg', 'component-task-bg', 'component-tool-bg');
editorTabLink.classList.add(`component-${componentType}`);
editorTab.classList.add(`component-${componentType}-bg`);
// Disable other tabs & main form elements
toggleOtherTabs(true);
toggleMainFormElements(true)
// Show editor tab and switch to it
editorTabLink.classList.remove('d-none');
editorTabLink.click();
})
.catch(error => {
console.error('Error fetching editor:', error);
alert('Error loading editor. Please try again.');
});
});
// Clean up color classes when returning from editor
editorTabLink.addEventListener('hide.bs.tab', function() {
editorTabLink.classList.remove('component-agent', 'component-task', 'component-tool');
editorTab.classList.remove('component-agent-bg', 'component-task-bg', 'component-tool-bg');
});
});
// Handle component editor form submissions
editorContent.addEventListener('click', function(e) {
if (e.target && e.target.classList.contains('component-submit')) {
e.preventDefault();
console.log('Submit button clicked');
// Get all form fields from the editor content
const formData = new FormData();
editorContent.querySelectorAll('input, textarea, select').forEach(field => {
if (field.type === 'checkbox') {
formData.append(field.name, field.checked ? 'y' : 'n');
} else {
formData.append(field.name, field.value);
}
});
// Add CSRF token
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
formData.append('csrf_token', csrfToken);
// Get the component ID from the current state
const selectedRow = document.querySelector('input[name="selected_row"]:checked');
const componentData = JSON.parse(selectedRow.value.replace(/'/g, '"'));
const componentId = componentData.value;
// Determine the component type (agent, task, or tool)
const componentType = editorTabLink.classList.contains('component-agent') ? 'agent' :
editorTabLink.classList.contains('component-task') ? 'task' : 'tool';
// Submit the data
fetch(`/admin/interaction/${componentType}/${componentId}/save`, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Handle success - reload the page
location.reload();
} else {
// Handle error
alert(data.message || 'Error saving component');
}
})
.catch(error => {
console.error('Error:', error);
alert('Error saving component');
});
}
});
// Handle case when editor tab is hidden
editorTabLink.addEventListener('hide.bs.tab', function() {
// Re-enable all tabs & main form elements
toggleOtherTabs(false);
toggleMainFormElements(false)
// Remove color classes
editorTabLink.classList.remove('component-agent', 'component-task', 'component-tool');
editorTab.classList.remove('component-agent-bg', 'component-task-bg', 'component-tool-bg');
});
// Function to handle canceling edit
function cancelEdit() {
// Re-enable all tabs & main form elements
toggleOtherTabs(false);
toggleMainFormElements()
// Return to previous tab
if (previousTab) {
previousTab.click();
}
// Hide the editor tab
editorTabLink.classList.add('d-none');
}
// Handle cancel button in editor
document.addEventListener('click', function(e) {
if (e.target && e.target.id === 'cancelEdit') {
// Get the previously active tab (stored before switching to editor)
const previousTab = document.querySelector('[href="#configuration-tab"]'); // default to configuration tab
cancelEdit()
// Hide the editor tab
document.getElementById('editor-tab-link').classList.add('d-none');
}
});
});
</script>
<style>
.tab-pane .card {
margin-bottom: 1rem;
}
.nav-link.component-agent,
.nav-link.component-task,
.nav-link.component-tool {
color: white !important;
}
/* Add new CSS for normal tabs */
.nav-link {
color: #344767 !important; /* Default dark color */
}
/* Style for disabled tabs */
.nav-link.disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
.component-agent {
background-color: #9c27b0 !important; /* Purple */
color: white !important;
}
.component-task {
background-color: #ff9800 !important; /* Orange */
color: white !important;
}
.component-tool {
background-color: #009688 !important; /* Teal */
color: white !important;
}
/* Lighter background versions for the tab content */
.tab-pane.component-agent-bg {
background-color: rgba(156, 39, 176, 0.2); /* Light purple */
}
.tab-pane.component-task-bg {
background-color: rgba(255, 152, 0, 0.2); /* Light orange */
}
.tab-pane.component-tool-bg {
background-color: rgba(0, 150, 136, 0.2); /* Light teal */
}
/* Add some padding to the tab content */
.tab-pane {
padding: 15px;
border-radius: 0.5rem;
}
.specialist-overview {
width: 100%;
height: auto;
min-height: 200px;
display: flex;
justify-content: center;
align-items: center;
}
.specialist-overview svg {
width: 100%;
height: auto;
max-height: 400px; /* Adjust as needed */
}
</style>
{% endblock %} {% endblock %}

View File

@@ -19,5 +19,5 @@
{% endblock %} {% endblock %}
{% block content_footer %} {% block content_footer %}
{{ render_pagination(pagination, 'document_bp.retrievers') }} {{ render_pagination(pagination, 'interaction_bp.specialists') }}
{% endblock %} {% endblock %}

View File

@@ -135,7 +135,7 @@
</div> </div>
{% endmacro %} {% endmacro %}
{% macro render_selectable_table(headers, rows, selectable, id) %} {% macro render_selectable_table(headers, rows, selectable, id, is_component_selector=False) %}
<div class="card"> <div class="card">
<div class="table-responsive"> <div class="table-responsive">
<table class="table align-items-center mb-0" id="{{ id }}"> <table class="table align-items-center mb-0" id="{{ id }}">
@@ -153,7 +153,16 @@
{% for row in rows %} {% for row in rows %}
<tr> <tr>
{% if selectable %} {% if selectable %}
<td><input type="radio" name="selected_row" value="{{ row[0] }}" required></td> <td>
<input type="radio"
name="selected_row"
value="{{ row[0] }}"
{% if is_component_selector %}
data-component-selector="true"
{% else %}
required
{% endif %}>
</td>
{% endif %} {% endif %}
{% for cell in row %} {% for cell in row %}
<td class="{{ cell.class }}"> <td class="{{ cell.class }}">

View File

@@ -21,7 +21,6 @@ def get_tools():
class SpecialistForm(FlaskForm): class SpecialistForm(FlaskForm):
name = StringField('Name', validators=[DataRequired(), Length(max=50)]) name = StringField('Name', validators=[DataRequired(), Length(max=50)])
description = TextAreaField('Description', validators=[DataRequired()])
retrievers = QuerySelectMultipleField( retrievers = QuerySelectMultipleField(
'Retrievers', 'Retrievers',
@@ -37,14 +36,14 @@ class SpecialistForm(FlaskForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
types_dict = cache_manager.specialist_config_cache.get_types() types_dict = cache_manager.specialists_types_cache.get_types()
# 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 types_dict.items()] self.type.choices = [(key, value['name']) for key, value in types_dict.items()]
class EditSpecialistForm(DynamicFormBase): class EditSpecialistForm(DynamicFormBase):
name = StringField('Name', validators=[DataRequired()]) name = StringField('Name', validators=[DataRequired()])
description = TextAreaField('Description', validators=[DataRequired()]) description = TextAreaField('Description', validators=[Optional()])
retrievers = QuerySelectMultipleField( retrievers = QuerySelectMultipleField(
'Retrievers', 'Retrievers',
@@ -55,6 +54,7 @@ class EditSpecialistForm(DynamicFormBase):
) )
type = StringField('Specialist Type', validators=[DataRequired()], render_kw={'readonly': True}) type = StringField('Specialist Type', validators=[DataRequired()], render_kw={'readonly': True})
type_version = StringField('Type Version', validators=[DataRequired()], render_kw={'readonly': True})
tuning = BooleanField('Enable Retrieval Tuning', default=False) tuning = BooleanField('Enable Retrieval Tuning', default=False)
@@ -76,41 +76,21 @@ class BaseEditComponentForm(DynamicFormBase):
name = StringField('Name', validators=[DataRequired()]) name = StringField('Name', validators=[DataRequired()])
description = TextAreaField('Description', validators=[Optional()]) description = TextAreaField('Description', validators=[Optional()])
type = StringField('Type', validators=[DataRequired()], render_kw={'readonly': True}) type = StringField('Type', validators=[DataRequired()], render_kw={'readonly': True})
type_version = StringField('Type Version', validators=[DataRequired()], render_kw={'readonly': True})
tuning = BooleanField('Enable Tuning', default=False) tuning = BooleanField('Enable Tuning', default=False)
class EveAIAgentForm(BaseComponentForm):
role = TextAreaField('Role', validators=[DataRequired()])
goal = TextAreaField('Goal', validators=[DataRequired()])
backstory = TextAreaField('Backstory', validators=[DataRequired()])
tools = QuerySelectMultipleField(
'Tools',
query_factory=get_tools,
get_label='name',
allow_blank=True,
description='Select one or more tools that can be used this agent'
)
def __init__(self, *args, type_config=None, **kwargs):
super().__init__(*args, **kwargs)
if type_config:
self.type.choices = [(key, value['name']) for key, value in type_config.items()]
class EditEveAIAgentForm(BaseEditComponentForm): class EditEveAIAgentForm(BaseEditComponentForm):
role = StringField('Role', validators=[DataRequired()]) role = TextAreaField('Role', validators=[Optional()])
goal = StringField('Goal', validators=[DataRequired()]) goal = TextAreaField('Goal', validators=[Optional()])
backstory = StringField('Backstory', validators=[DataRequired()]) backstory = TextAreaField('Backstory', validators=[Optional()])
tools = QuerySelectMultipleField(
'Tools',
query_factory=get_tools,
get_label='name',
allow_blank=True,
description='Select one or more tools that can be used this agent'
)
class EveAITaskForm(BaseComponentForm): class EditEveAITaskForm(BaseEditComponentForm):
expected_output = TextAreaField('Expected Output', validators=[DataRequired()]) task_description = StringField('Task Description', validators=[Optional()])
expected_outcome = StringField('Expected Outcome', validators=[Optional()])
class EditEveAIToolForm(BaseEditComponentForm):
pass

View File

@@ -1,13 +1,15 @@
import ast import ast
from datetime import datetime as dt, timezone as tz from datetime import datetime as dt, timezone as tz
from flask import request, redirect, flash, render_template, Blueprint, session, current_app from flask import request, redirect, flash, render_template, Blueprint, session, current_app, jsonify, url_for
from flask_security import roles_accepted from flask_security import roles_accepted
from langchain.agents import Agent
from sqlalchemy import desc from sqlalchemy import desc
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from common.models.document import Embedding, DocumentVersion, Retriever from common.models.document import Embedding, DocumentVersion, Retriever
from common.models.interaction import (ChatSession, Interaction, InteractionEmbedding, Specialist, SpecialistRetriever) from common.models.interaction import (ChatSession, Interaction, InteractionEmbedding, Specialist, SpecialistRetriever,
EveAIAgent, EveAITask, EveAITool)
from common.extensions import db, cache_manager from common.extensions import db, cache_manager
from common.utils.model_logging_utils import set_logging_information, update_logging_information from common.utils.model_logging_utils import set_logging_information, update_logging_information
@@ -19,7 +21,8 @@ from common.utils.specialist_utils import initialize_specialist
from config.type_defs.specialist_types import SPECIALIST_TYPES from config.type_defs.specialist_types import SPECIALIST_TYPES
from .interaction_forms import (SpecialistForm, EditSpecialistForm) from .interaction_forms import (SpecialistForm, EditSpecialistForm, EditEveAIAgentForm, EditEveAITaskForm,
EditEveAIToolForm)
interaction_bp = Blueprint('interaction_bp', __name__, url_prefix='/interaction') interaction_bp = Blueprint('interaction_bp', __name__, url_prefix='/interaction')
@@ -140,7 +143,7 @@ def specialist():
new_specialist.name = form.name.data new_specialist.name = form.name.data
new_specialist.description = form.description.data new_specialist.description = form.description.data
new_specialist.type = form.type.data new_specialist.type = form.type.data
new_specialist.type_version = cache_manager.specialist_config_cache.get_latest_version(new_specialist.type) new_specialist.type_version = cache_manager.specialists_version_tree_cache.get_latest_version(new_specialist.type)
new_specialist.tuning = form.tuning.data new_specialist.tuning = form.tuning.data
set_logging_information(new_specialist, dt.now(tz.utc)) set_logging_information(new_specialist, dt.now(tz.utc))
@@ -182,16 +185,29 @@ def edit_specialist(specialist_id):
specialist = Specialist.query.get_or_404(specialist_id) specialist = Specialist.query.get_or_404(specialist_id)
form = EditSpecialistForm(request.form, obj=specialist) form = EditSpecialistForm(request.form, obj=specialist)
specialist_config = cache_manager.specialist_config_cache.get_config(specialist.type, specialist.type_version) specialist_config = cache_manager.specialists_config_cache.get_config(specialist.type, specialist.type_version)
configuration_config = specialist_config.get('configuration') configuration_config = specialist_config.get('configuration')
form.add_dynamic_fields("configuration", configuration_config, specialist.configuration) form.add_dynamic_fields("configuration", configuration_config, specialist.configuration)
agent_rows = prepare_table_for_macro(specialist.agents,
[('id', ''), ('name', ''), ('type', ''), ('type_version', '')])
task_rows = prepare_table_for_macro(specialist.tasks,
[('id', ''), ('name', ''), ('type', ''), ('type_version', '')])
tool_rows = prepare_table_for_macro(specialist.tools,
[('id', ''), ('name', ''), ('type', ''), ('type_version', '')])
# Construct the SVG overview path
svg_filename = f"{specialist.type}_{specialist.type_version}_overview.svg"
svg_path = url_for('static', filename=f'assets/specialists/{svg_filename}')
if request.method == 'GET': if request.method == 'GET':
# Get the actual Retriever objects for the associated retriever_ids # Get the actual Retriever objects for the associated retriever_ids
retriever_objects = Retriever.query.filter( retriever_objects = Retriever.query.filter(
Retriever.id.in_([sr.retriever_id for sr in specialist.retrievers]) Retriever.id.in_([sr.retriever_id for sr in specialist.retrievers])
).all() ).all()
form.retrievers.data = retriever_objects form.retrievers.data = retriever_objects
if specialist.description is None:
specialist.description = specialist_config.get('metadata').get('description', '')
if form.validate_on_submit(): if form.validate_on_submit():
# Update the basic fields # Update the basic fields
@@ -230,11 +246,25 @@ def edit_specialist(specialist_id):
db.session.rollback() db.session.rollback()
flash(f'Failed to update specialist. Error: {str(e)}', 'danger') flash(f'Failed to update specialist. Error: {str(e)}', 'danger')
current_app.logger.error(f'Failed to update specialist {specialist_id}. Error: {str(e)}') current_app.logger.error(f'Failed to update specialist {specialist_id}. Error: {str(e)}')
return render_template('interaction/edit_specialist.html', form=form, specialist_id=specialist_id) return render_template('interaction/edit_specialist.html',
form=form,
specialist_id=specialist_id,
agent_rows=agent_rows,
task_rows=task_rows,
tool_rows=tool_rows,
prefixed_url_for=prefixed_url_for,
svg_path=svg_path,)
else: else:
form_validation_failed(request, form) form_validation_failed(request, form)
return render_template('interaction/edit_specialist.html', form=form, specialist_id=specialist_id) return render_template('interaction/edit_specialist.html',
form=form,
specialist_id=specialist_id,
agent_rows=agent_rows,
task_rows=task_rows,
tool_rows=tool_rows,
prefixed_url_for=prefixed_url_for,
svg_path=svg_path,)
@interaction_bp.route('/specialists', methods=['GET', 'POST']) @interaction_bp.route('/specialists', methods=['GET', 'POST'])
@@ -268,3 +298,154 @@ def handle_specialist_selection():
return redirect(prefixed_url_for('interaction_bp.specialists')) return redirect(prefixed_url_for('interaction_bp.specialists'))
# Routes for Agent management
@interaction_bp.route('/agent/<int:agent_id>/edit', methods=['GET'])
@roles_accepted('Super User', 'Tenant Admin')
def edit_agent(agent_id):
agent = EveAIAgent.query.get_or_404(agent_id)
form = EditEveAIAgentForm(obj=agent)
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
# Return just the form portion for AJAX requests
return render_template('interaction/components/edit_agent.html',
form=form,
agent=agent,
title="Edit Agent",
description="Configure the agent with company-specific details if required",
submit_text="Save Agent")
@interaction_bp.route('/agent/<int:agent_id>/save', methods=['POST'])
@roles_accepted('Super User', 'Tenant Admin')
def save_agent(agent_id):
agent = EveAIAgent.query.get_or_404(agent_id) if agent_id else EveAIAgent()
tenant_id = session.get('tenant').get('id')
form = EditEveAIAgentForm(obj=agent)
if form.validate_on_submit():
try:
form.populate_obj(agent)
update_logging_information(agent, dt.now(tz.utc))
if not agent_id: # New agent
db.session.add(agent)
db.session.commit()
return jsonify({'success': True, 'message': 'Agent saved successfully'})
except Exception as e:
db.session.rollback()
current_app.logger.error(f'Failed to save agent {agent_id} for tenant {tenant_id}. Error: {str(e)}')
return jsonify({'success': False, 'message': f"Failed to save agent {agent_id}: {str(e)}"})
return jsonify({'success': False, 'message': 'Validation failed'})
# Routes for Task management
@interaction_bp.route('/task/<int:task_id>/edit', methods=['GET'])
@roles_accepted('Super User', 'Tenant Admin')
def edit_task(task_id):
task = EveAITask.query.get_or_404(task_id)
form = EditEveAITaskForm(obj=task)
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return render_template('interaction/components/edit_task.html',
form=form,
task=task)
@interaction_bp.route('/task/<int:task_id>/save', methods=['POST'])
@roles_accepted('Super User', 'Tenant Admin')
def save_task(task_id):
task = EveAITask.query.get_or_404(task_id) if task_id else EveAITask()
tenant_id = session.get('tenant').get('id')
form = EditEveAITaskForm(obj=task) # Replace with actual task form
if form.validate_on_submit():
try:
form.populate_obj(task)
update_logging_information(task, dt.now(tz.utc))
if not task_id: # New task
db.session.add(task)
db.session.commit()
return jsonify({'success': True, 'message': 'Task saved successfully'})
except Exception as e:
db.session.rollback()
current_app.logger.error(f'Failed to save task {task_id} for tenant {tenant_id}. Error: {str(e)}')
return jsonify({'success': False, 'message': f"Failed to save task {task_id}: {str(e)}"})
return jsonify({'success': False, 'message': 'Validation failed'})
# Routes for Tool management
@interaction_bp.route('/tool/<int:tool_id>/edit', methods=['GET'])
@roles_accepted('Super User', 'Tenant Admin')
def edit_tool(tool_id):
tool = EveAITool.query.get_or_404(tool_id)
form = EditEveAIToolForm(obj=tool)
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return render_template('interaction/components/edit_tool.html',
form=form,
tool=tool)
@interaction_bp.route('/tool/<int:tool_id>/save', methods=['POST'])
@roles_accepted('Super User', 'Tenant Admin')
def save_tool(tool_id):
tool = EveAITool.query.get_or_404(tool_id) if tool_id else EveAITool()
tenant_id = session.get('tenant').get('id')
form = EditEveAIToolForm(obj=tool) # Replace with actual tool form
if form.validate_on_submit():
try:
form.populate_obj(tool)
update_logging_information(tool, dt.now(tz.utc))
if not tool_id: # New tool
db.session.add(tool)
db.session.commit()
return jsonify({'success': True, 'message': 'Tool saved successfully'})
except Exception as e:
db.session.rollback()
current_app.logger.error(f'Failed to save tool {tool_id} for tenant {tenant_id}. Error: {str(e)}')
return jsonify({'success': False, 'message': f"Failed to save tool {tool_id}: {str(e)}"})
return jsonify({'success': False, 'message': 'Validation failed'})
# Component selection handlers
@interaction_bp.route('/handle_agent_selection', methods=['POST'])
@roles_accepted('Super User', 'Tenant Admin')
def handle_agent_selection():
agent_identification = request.form['selected_row']
agent_id = ast.literal_eval(agent_identification).get('value')
action = request.form.get('action')
if action == "edit_agent":
return redirect(prefixed_url_for('interaction_bp.edit_agent', agent_id=agent_id))
return redirect(prefixed_url_for('interaction_bp.edit_specialist'))
@interaction_bp.route('/handle_task_selection', methods=['POST'])
@roles_accepted('Super User', 'Tenant Admin')
def handle_task_selection():
task_identification = request.form['selected_row']
task_id = ast.literal_eval(task_identification).get('value')
action = request.form.get('action')
if action == "edit_task":
return redirect(prefixed_url_for('interaction_bp.edit_task', task_id=task_id))
return redirect(prefixed_url_for('interaction_bp.edit_specialist'))
@interaction_bp.route('/handle_tool_selection', methods=['POST'])
@roles_accepted('Super User', 'Tenant Admin')
def handle_tool_selection():
tool_identification = request.form['selected_row']
tool_id = ast.literal_eval(tool_identification).get('value')
action = request.form.get('action')
if action == "edit_tool":
return redirect(prefixed_url_for('interaction_bp.edit_tool', tool_id=tool_id))
return redirect(prefixed_url_for('interaction_bp.edit_specialist'))

View File

@@ -0,0 +1,29 @@
"""Add task_description to EveAITask model
Revision ID: efcd6a0d2989
Revises: 1e8ed0bd9662
Create Date: 2025-01-20 08:08:48.401704
"""
from alembic import op
import sqlalchemy as sa
import pgvector
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'efcd6a0d2989'
down_revision = '1e8ed0bd9662'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('eve_ai_task', sa.Column('task_description', sa.Text(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('eve_ai_task', 'task_description')
# ### end Alembic commands ###

Binary file not shown.

Before

Width:  |  Height:  |  Size: 809 B

After

Width:  |  Height:  |  Size: 264 KiB

View File

@@ -50,7 +50,7 @@ python-iso639~=2024.4.27
python-magic~=0.4.27 python-magic~=0.4.27
python-socketio~=5.11.3 python-socketio~=5.11.3
pytz~=2024.1 pytz~=2024.1
PyYAML~=6.0.2rc1 PyYAML~=6.0.2
redis~=5.0.4 redis~=5.0.4
requests~=2.32.3 requests~=2.32.3
SQLAlchemy~=2.0.35 SQLAlchemy~=2.0.35

View File

@@ -3,7 +3,9 @@
rm -f *repo.txt rm -f *repo.txt
# Define the list of components # Define the list of components
components=("docker" "eveai_api" "eveai_app" "eveai_app_startup" "eveai_beat" "eveai_chat" "eveai_chat_workers" "eveai_entitlements" "eveai_workers" "nginx" "full" "integrations") components=("docker" "eveai_api" "eveai_app" "eveai_app_startup" "eveai_beat" "eveai_chat" "eveai_chat_workers"
"eveai_entitlements" "eveai_workers" "nginx" "full" "integrations" "eveai_app_documents" "eveai_app_entitlements"
"eveai_app_interaction" "eveai_app_user")
# Get the current date and time in the format YYYY-MM-DD_HH-MM # Get the current date and time in the format YYYY-MM-DD_HH-MM
timestamp=$(date +"%Y-%m-%d_%H-%M") timestamp=$(date +"%Y-%m-%d_%H-%M")

View File

@@ -12,4 +12,4 @@ export FLASK_APP=${PROJECT_DIR}/scripts/run_eveai_app.py # Adjust the path to y
chown -R appuser:appuser /app/logs chown -R appuser:appuser /app/logs
# Start Flask app # Start Flask app
gunicorn -w 1 -k geventwebsocket.gunicorn.workers.GeventWebSocketWorker -b 0.0.0.0:5001 --worker-connections 100 scripts.run_eveai_api:app gunicorn -w 1 -k gevent -b 0.0.0.0:5001 --worker-connections 100 scripts.run_eveai_api:app

View File

@@ -49,4 +49,4 @@ python ${PROJECT_DIR}/scripts/initialize_data.py # Adjust the path to your init
# Start Flask app # Start Flask app
# gunicorn -w 1 -k gevent -b 0.0.0.0:5001 --worker-connections 100 scripts.run_eveai_app:app # gunicorn -w 1 -k gevent -b 0.0.0.0:5001 --worker-connections 100 scripts.run_eveai_app:app
gunicorn -w 1 -k geventwebsocket.gunicorn.workers.GeventWebSocketWorker -b 0.0.0.0:5001 --worker-connections 100 scripts.run_eveai_app:app gunicorn -w 1 -k gevent -b 0.0.0.0:5001 --worker-connections 100 scripts.run_eveai_app:app

View File

@@ -13,4 +13,4 @@ chown -R appuser:appuser /app/logs
echo "Starting EveAI Chat" echo "Starting EveAI Chat"
# Start Flask app # Start Flask app
gunicorn -w 1 -k geventwebsocket.gunicorn.workers.GeventWebSocketWorker -b 0.0.0.0:5001 --worker-connections 100 scripts.run_eveai_chat:app gunicorn -w 1 -k gevent -b 0.0.0.0:5001 --worker-connections 100 scripts.run_eveai_chat:app