- 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:
@@ -13,6 +13,7 @@ migrations/
|
||||
*material*
|
||||
*nucleo*
|
||||
*package*
|
||||
*.svg
|
||||
nginx/mime.types
|
||||
*.gitignore*
|
||||
.python-version
|
||||
|
||||
28
.repopackignore_eveai_app_documents
Normal file
28
.repopackignore_eveai_app_documents
Normal 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*
|
||||
28
.repopackignore_eveai_app_entitlements
Normal file
28
.repopackignore_eveai_app_entitlements
Normal 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*
|
||||
23
.repopackignore_eveai_app_interaction
Normal file
23
.repopackignore_eveai_app_interaction
Normal 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*
|
||||
28
.repopackignore_eveai_app_user
Normal file
28
.repopackignore_eveai_app_user
Normal 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*
|
||||
@@ -73,6 +73,7 @@ class EveAITask(db.Model):
|
||||
description = db.Column(db.Text, nullable=True)
|
||||
type = db.Column(db.String(50), nullable=False, default="STANDARD_RAG")
|
||||
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)
|
||||
tuning = db.Column(db.Boolean, nullable=True, default=False)
|
||||
configuration = db.Column(JSONB, nullable=True)
|
||||
|
||||
75
common/utils/cache/base.py
vendored
75
common/utils/cache/base.py
vendored
@@ -1,7 +1,8 @@
|
||||
from typing import Any, Dict, List, Optional, TypeVar, Generic, Type
|
||||
from dataclasses import dataclass
|
||||
from flask import Flask
|
||||
from flask import Flask, current_app
|
||||
from dogpile.cache import CacheRegion
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
T = TypeVar('T') # Generic type parameter for cached data
|
||||
|
||||
@@ -47,6 +48,46 @@ class CacheHandler(Generic[T]):
|
||||
self.prefix = prefix
|
||||
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):
|
||||
"""
|
||||
Configure required components for cache key generation.
|
||||
@@ -77,8 +118,13 @@ class CacheHandler(Generic[T]):
|
||||
if 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})
|
||||
return f"{self.prefix}:{str(key)}"
|
||||
return f"{region_name}_{self.prefix}:{str(key)}"
|
||||
|
||||
def get(self, creator_func, **identifiers) -> T:
|
||||
"""
|
||||
@@ -92,18 +138,19 @@ class CacheHandler(Generic[T]):
|
||||
Cached or newly created value
|
||||
"""
|
||||
cache_key = self.generate_key(**identifiers)
|
||||
current_app.logger.debug(f"Cache key: {cache_key}")
|
||||
|
||||
def creator():
|
||||
instance = creator_func(**identifiers)
|
||||
return self.to_cache_data(instance)
|
||||
return self._to_cache_data(instance)
|
||||
|
||||
cached_data = self.region.get_or_create(
|
||||
cache_key,
|
||||
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):
|
||||
"""
|
||||
@@ -128,3 +175,21 @@ class CacheHandler(Generic[T]):
|
||||
except ValueError:
|
||||
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.")
|
||||
|
||||
370
common/utils/cache/config_cache.py
vendored
370
common/utils/cache/config_cache.py
vendored
@@ -5,11 +5,15 @@ from packaging import version
|
||||
import os
|
||||
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
|
||||
|
||||
|
||||
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]]):
|
||||
"""Base handler for configuration caching"""
|
||||
|
||||
@@ -23,18 +27,111 @@ class BaseConfigCacheHandler(CacheHandler[Dict[str, Any]]):
|
||||
self.config_type = config_type
|
||||
self._types_module = 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):
|
||||
"""Configure required keys based on operation"""
|
||||
match operation:
|
||||
case 'get_types':
|
||||
self.configure_keys('type_name') # Only require type_name for type definitions
|
||||
case 'get_versions':
|
||||
self.configure_keys('type_name') # Only type_name needed for version tree
|
||||
case 'get_config':
|
||||
self.configure_keys('type_name', 'version') # Both needed for specific config
|
||||
case _:
|
||||
raise ValueError(f"Unknown operation: {operation}")
|
||||
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 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]:
|
||||
"""
|
||||
@@ -46,6 +143,7 @@ class BaseConfigCacheHandler(CacheHandler[Dict[str, Any]]):
|
||||
Returns:
|
||||
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
|
||||
if not type_path.exists():
|
||||
raise ValueError(f"No configuration found for type {type_name}")
|
||||
@@ -81,25 +179,25 @@ class BaseConfigCacheHandler(CacheHandler[Dict[str, Any]]):
|
||||
continue
|
||||
|
||||
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 {
|
||||
'versions': versions,
|
||||
'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"""
|
||||
# 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]:
|
||||
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:
|
||||
def _should_cache(self, value: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Validate if the value should be cached
|
||||
|
||||
@@ -109,65 +207,7 @@ class BaseConfigCacheHandler(CacheHandler[Dict[str, Any]]):
|
||||
Returns:
|
||||
bool: True if the value should be cached
|
||||
"""
|
||||
if not isinstance(value, dict):
|
||||
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}")
|
||||
return isinstance(value, dict) # Cache all dictionaries
|
||||
|
||||
def get_versions(self, type_name: str) -> Dict[str, Any]:
|
||||
"""
|
||||
@@ -179,7 +219,7 @@ class BaseConfigCacheHandler(CacheHandler[Dict[str, Any]]):
|
||||
Returns:
|
||||
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(
|
||||
lambda type_name: self._load_version_tree(type_name),
|
||||
type_name=type_name
|
||||
@@ -235,72 +275,146 @@ class BaseConfigCacheHandler(CacheHandler[Dict[str, Any]]):
|
||||
latest_patch = max(matching_versions, key=version.parse)
|
||||
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]]:
|
||||
"""Get dictionary of available types with name and description"""
|
||||
self.configure_keys_for_operation('get_types')
|
||||
result = self.get(
|
||||
current_app.logger.debug(f"Trying to retrieve type definitions for {self.config_type}")
|
||||
result = self.get(
|
||||
lambda type_name: self._load_type_definitions(),
|
||||
type_name=f'{self.config_type}_types',
|
||||
)
|
||||
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:
|
||||
type_name: Configuration type name
|
||||
version: Optional specific version to retrieve
|
||||
|
||||
Returns:
|
||||
Configuration data
|
||||
"""
|
||||
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
|
||||
)
|
||||
def create_config_cache_handlers(config_type: str, config_dir: str, types_module: dict) -> tuple:
|
||||
"""
|
||||
Factory function to dynamically create the 3 cache handler classes for a given configuration type.
|
||||
The following cache names are created:
|
||||
- <config_type>_config_cache
|
||||
- <config_type>_version_tree_cache
|
||||
- <config_type>_types_cache
|
||||
|
||||
|
||||
class AgentConfigCacheHandler(BaseConfigCacheHandler):
|
||||
"""Handler for agent configurations"""
|
||||
handler_name = 'agent_config_cache'
|
||||
Args:
|
||||
config_type: The configuration type (e.g., 'agents', 'tasks').
|
||||
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):
|
||||
super().__init__(region, 'agents')
|
||||
self._types_module = agent_types.AGENT_TYPES
|
||||
self._config_dir = os.path.join('config', 'agents')
|
||||
Returns:
|
||||
A tuple of dynamically created classes for config, version tree, and types handlers.
|
||||
"""
|
||||
|
||||
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):
|
||||
"""Handler for task configurations"""
|
||||
handler_name = 'task_config_cache'
|
||||
|
||||
def __init__(self, region):
|
||||
super().__init__(region, 'tasks')
|
||||
self._types_module = task_types.TASK_TYPES
|
||||
self._config_dir = os.path.join('config', 'tasks')
|
||||
AgentConfigCacheHandler, AgentConfigVersionTreeCacheHandler, AgentConfigTypesCacheHandler = (
|
||||
create_config_cache_handlers(
|
||||
config_type='agents',
|
||||
config_dir='config/agents',
|
||||
types_module=agent_types.AGENT_TYPES
|
||||
))
|
||||
|
||||
|
||||
class ToolConfigCacheHandler(BaseConfigCacheHandler):
|
||||
"""Handler for tool configurations"""
|
||||
handler_name = 'tool_config_cache'
|
||||
|
||||
def __init__(self, region):
|
||||
super().__init__(region, 'tools')
|
||||
self._types_module = tool_types.TOOL_TYPES
|
||||
self._config_dir = os.path.join('config', 'tools')
|
||||
TaskConfigCacheHandler, TaskConfigVersionTreeCacheHandler, TaskConfigTypesCacheHandler = (
|
||||
create_config_cache_handlers(
|
||||
config_type='tasks',
|
||||
config_dir='config/tasks',
|
||||
types_module=task_types.TASK_TYPES
|
||||
))
|
||||
|
||||
|
||||
class SpecialistConfigCacheHandler(BaseConfigCacheHandler):
|
||||
"""Handler for specialist configurations"""
|
||||
handler_name = 'specialist_config_cache'
|
||||
ToolConfigCacheHandler, ToolConfigVersionTreeCacheHandler, ToolConfigTypesCacheHandler = (
|
||||
create_config_cache_handlers(
|
||||
config_type='tools',
|
||||
config_dir='config/tools',
|
||||
types_module=tool_types.TOOL_TYPES
|
||||
))
|
||||
|
||||
def __init__(self, region):
|
||||
super().__init__(region, 'specialists')
|
||||
self._types_module = specialist_types.SPECIALIST_TYPES
|
||||
self._config_dir = os.path.join('config', 'specialists')
|
||||
|
||||
SpecialistConfigCacheHandler, SpecialistConfigVersionTreeCacheHandler, SpecialistConfigTypesCacheHandler = (
|
||||
create_config_cache_handlers(
|
||||
config_type='specialists',
|
||||
config_dir='config/specialists',
|
||||
types_module=specialist_types.SPECIALIST_TYPES
|
||||
))
|
||||
|
||||
6
common/utils/cache/regions.py
vendored
6
common/utils/cache/regions.py
vendored
@@ -65,11 +65,7 @@ def create_cache_regions(app):
|
||||
|
||||
eveai_config_region = make_region(name='eveai_config').configure(
|
||||
'dogpile.cache.redis',
|
||||
arguments={
|
||||
**redis_config,
|
||||
'redis_expiration_time': None, # No expiration in Redis
|
||||
'key_mangler': lambda key: f"startup_{startup_time}:{key}" # Prefix all keys
|
||||
},
|
||||
arguments=redis_config,
|
||||
replace_existing_backend=True
|
||||
)
|
||||
regions['eveai_config'] = eveai_config_region
|
||||
|
||||
@@ -23,7 +23,7 @@ def initialize_specialist(specialist_id: int, specialist_type: str, specialist_v
|
||||
ValueError: If specialist not found or invalid configuration
|
||||
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:
|
||||
raise ValueError(f"No configuration found for {specialist_type} version {specialist_version}")
|
||||
if config['framework'] == 'langchain':
|
||||
@@ -99,16 +99,18 @@ def _create_agent(
|
||||
timestamp: Optional[dt] = None
|
||||
) -> EveAIAgent:
|
||||
"""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:
|
||||
timestamp = dt.now(tz=tz.utc)
|
||||
|
||||
# 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(
|
||||
specialist_id=specialist_id,
|
||||
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_version=agent_version,
|
||||
role=None,
|
||||
@@ -122,6 +124,7 @@ def _create_agent(
|
||||
set_logging_information(agent, timestamp)
|
||||
|
||||
db.session.add(agent)
|
||||
current_app.logger.info(f"Created agent {agent.id} of type {agent_type}")
|
||||
return agent
|
||||
|
||||
|
||||
@@ -138,14 +141,16 @@ def _create_task(
|
||||
timestamp = dt.now(tz=tz.utc)
|
||||
|
||||
# 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(
|
||||
specialist_id=specialist_id,
|
||||
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_version=task_version,
|
||||
task_description=None,
|
||||
expected_output=None,
|
||||
tuning=False,
|
||||
configuration=None,
|
||||
@@ -157,6 +162,7 @@ def _create_task(
|
||||
set_logging_information(task, timestamp)
|
||||
|
||||
db.session.add(task)
|
||||
current_app.logger.info(f"Created task {task.id} of type {task_type}")
|
||||
return task
|
||||
|
||||
|
||||
@@ -173,12 +179,13 @@ def _create_tool(
|
||||
timestamp = dt.now(tz=tz.utc)
|
||||
|
||||
# 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(
|
||||
specialist_id=specialist_id,
|
||||
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_version=tool_version,
|
||||
tuning=False,
|
||||
@@ -189,4 +196,5 @@ def _create_tool(
|
||||
set_logging_information(tool, timestamp)
|
||||
|
||||
db.session.add(tool)
|
||||
current_app.logger.info(f"Created tool {tool.id} of type {tool_type}")
|
||||
return tool
|
||||
|
||||
@@ -28,10 +28,15 @@ def perform_startup_invalidation(app):
|
||||
try:
|
||||
# Check if invalidation was already performed
|
||||
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
|
||||
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")
|
||||
else:
|
||||
app.logger.info("Startup cache invalidation already performed")
|
||||
|
||||
@@ -166,7 +166,7 @@ class DevConfig(Config):
|
||||
DEVELOPMENT = True
|
||||
DEBUG = True
|
||||
FLASK_DEBUG = True
|
||||
EXPLAIN_TEMPLATE_LOADING = True
|
||||
EXPLAIN_TEMPLATE_LOADING = False
|
||||
|
||||
# Database Settings
|
||||
DB_HOST = environ.get('DB_HOST', 'localhost')
|
||||
|
||||
@@ -89,37 +89,38 @@ results:
|
||||
type: "List[str]"
|
||||
description: "A list of needs"
|
||||
required: false
|
||||
agents:
|
||||
- type: "RAG_AGENT"
|
||||
version: 1.0
|
||||
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.
|
||||
- type: "SPIN_DETECTION_AGENT"
|
||||
version: 1.0
|
||||
- type: "SPIN_SALES_SPECIALIST_AGENT"
|
||||
version: 1.0
|
||||
- type: "IDENTIFICATION_AGENT"
|
||||
version: 1.0
|
||||
- type: "EMAIL_CONTENT_AGENT"
|
||||
version: 1.0
|
||||
- type: "EMAIL_LEAD_ENGAGEMENT"
|
||||
version: 1.0
|
||||
tasks:
|
||||
- type: "RAG_TASK"
|
||||
version: 1.0
|
||||
- type: "SPIN_DETECT_TASK"
|
||||
version: 1.0
|
||||
- type: "SPIN_QUESTIONS_TASK"
|
||||
version: 1.0
|
||||
- type: "IDENTIFICATION_DETECTION_TASK"
|
||||
version: 1.0
|
||||
- type: "IDENTIFICATION_QUESTIONS_TASK"
|
||||
version: 1.0
|
||||
- type: "EMAIL_LEAD_DRAFTING"
|
||||
version: 1.0
|
||||
- type: "EMAIL_LEAD_ENGAGEMENT"
|
||||
version: 1.0
|
||||
agents:
|
||||
- type: "RAG_AGENT"
|
||||
version: "1.0"
|
||||
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.
|
||||
- type: "SPIN_DETECTION_AGENT"
|
||||
version: "1.0"
|
||||
- type: "SPIN_SALES_SPECIALIST_AGENT"
|
||||
version: "1.0"
|
||||
- type: "IDENTIFICATION_AGENT"
|
||||
version: "1.0"
|
||||
- type: "EMAIL_CONTENT_AGENT"
|
||||
version: "1.0"
|
||||
- type: "EMAIL_ENGAGEMENT_AGENT"
|
||||
version: "1.0"
|
||||
tasks:
|
||||
- type: "RAG_TASK"
|
||||
version: "1.0"
|
||||
- type: "SPIN_DETECT_TASK"
|
||||
version: "1.0"
|
||||
- type: "SPIN_QUESTIONS_TASK"
|
||||
version: "1.0"
|
||||
- type: "IDENTIFICATION_DETECTION_TASK"
|
||||
version: "1.0"
|
||||
- type: "IDENTIFICATION_QUESTIONS_TASK"
|
||||
version: "1.0"
|
||||
- type: "EMAIL_LEAD_DRAFTING_TASK"
|
||||
version: "1.0"
|
||||
- type: "EMAIL_LEAD_ENGAGEMENT_TASK"
|
||||
version: "1.0"
|
||||
metadata:
|
||||
author: "Josako"
|
||||
date_added: "2025-01-08"
|
||||
changes: "Initial version"
|
||||
description: "A Specialist that performs both Q&A as SPIN (Sales Process) activities"
|
||||
@@ -49,3 +49,4 @@ metadata:
|
||||
author: "Josako"
|
||||
date_added: "2025-01-08"
|
||||
changes: "Initial version"
|
||||
description: "A Specialist that performs standard Q&A"
|
||||
@@ -1,6 +1,6 @@
|
||||
version: "1.0.0"
|
||||
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
|
||||
company achievements when available. The email should speak directly to the lead's interests and the needs
|
||||
of their company.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
version: "1.0.0"
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
version: "1.0.0"
|
||||
name: "Identification Gathering"
|
||||
description: >
|
||||
task_description: >
|
||||
Detect and pass on identification information in the ongoing conversation, from within the following information:
|
||||
{question}
|
||||
Add to or refine the following already gathered identification information (between triple $)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
version: "1.0.0"
|
||||
name: "Define Identification Questions"
|
||||
description: >
|
||||
task_description: >
|
||||
Ask questions to complete or confirm the identification information gathered.
|
||||
Current Identification Information:
|
||||
$$${Identification}$$$
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
version: "1.0.0"
|
||||
name: "RAG Task"
|
||||
description: >
|
||||
task_description: >
|
||||
Answer the question based on the following context, delimited between triple backquotes, and taking into account
|
||||
the history of the discussion, in between triple %
|
||||
{custom_description}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
version: "1.0.0"
|
||||
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
|
||||
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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
version: "1.0.0"
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -158,6 +158,9 @@ docker buildx use eveai_builder
|
||||
|
||||
# Loop through services
|
||||
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 process_service "$SERVICE"; then
|
||||
echo "Successfully processed $SERVICE"
|
||||
|
||||
60
docker/copy_specialist_svgs.sh
Executable file
60
docker/copy_specialist_svgs.sh
Executable 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"
|
||||
@@ -9,11 +9,14 @@ COPY ../../nginx/mime.types /etc/nginx/mime.types
|
||||
|
||||
# Copy static & public files
|
||||
RUN mkdir -p /etc/nginx/static /etc/nginx/public
|
||||
|
||||
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/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-sdk.js /etc/nginx/static/js
|
||||
|
||||
# Copy public files
|
||||
COPY ../../nginx/public /etc/nginx/public
|
||||
|
||||
# Copy site-specific configurations
|
||||
|
||||
@@ -161,10 +161,31 @@ def register_blueprints(app):
|
||||
|
||||
def register_cache_handlers(app):
|
||||
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(AgentConfigTypesCacheHandler, 'eveai_config')
|
||||
cache_manager.register_handler(AgentConfigVersionTreeCacheHandler, '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(ToolConfigTypesCacheHandler, 'eveai_config')
|
||||
cache_manager.register_handler(ToolConfigVersionTreeCacheHandler, '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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,31 +1,28 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from "macros.html" import render_field %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block content_title %}{{ title }}{% endblock %}
|
||||
{% block content_description %}{{ description }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
{% set disabled_fields = [] %}
|
||||
{% set exclude_fields = [] %}
|
||||
{% for field in form.get_static_fields() %}
|
||||
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||
{% endfor %}
|
||||
{% if form.get_dynamic_fields is defined %}
|
||||
{% 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 %}
|
||||
{% set disabled_fields = [] %}
|
||||
{% set exclude_fields = [] %}
|
||||
{% for field in form.get_static_fields() %}
|
||||
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||
{% endfor %}
|
||||
{% if form.get_dynamic_fields is defined %}
|
||||
{% 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 %}
|
||||
{% endif %}
|
||||
<button type="submit" class="btn btn-primary">{{ submit_text }}</button>
|
||||
</form>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<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 %}
|
||||
|
||||
{% block content_footer %}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{% extends "interaction/component.html" %}
|
||||
@@ -1,33 +1,479 @@
|
||||
{% extends 'base.html' %}
|
||||
{% from "macros.html" import render_field %}
|
||||
{% from "macros.html" import render_field, render_selectable_table %}
|
||||
|
||||
{% block 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 %}
|
||||
<form method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
{% set disabled_fields = ['type'] %}
|
||||
{% set exclude_fields = [] %}
|
||||
<!-- Render Static Fields -->
|
||||
{% for field in form.get_static_fields() %}
|
||||
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||
{% endfor %}
|
||||
<!-- Render Dynamic Fields -->
|
||||
{% 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 %}
|
||||
<button type="submit" class="btn btn-primary">Save Specialist</button>
|
||||
</form>
|
||||
<div class="container-fluid px-0">
|
||||
<div class="row">
|
||||
<!-- Main Specialist Editor -->
|
||||
<div class="col-12" id="mainEditorSection">
|
||||
<form method="post" id="specialistForm" action="{{ url_for('interaction_bp.edit_specialist', specialist_id=specialist_id) }}">
|
||||
{{ form.hidden_tag() }}
|
||||
{% set disabled_fields = ['type', 'type_version'] %}
|
||||
{% set exclude_fields = [] %}
|
||||
<!-- Render Static Fields -->
|
||||
{% for field in form.get_static_fields() %}
|
||||
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||
{% endfor %}
|
||||
|
||||
<!-- Overview Section -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<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 %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
|
||||
@@ -19,5 +19,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content_footer %}
|
||||
{{ render_pagination(pagination, 'document_bp.retrievers') }}
|
||||
{{ render_pagination(pagination, 'interaction_bp.specialists') }}
|
||||
{% endblock %}
|
||||
@@ -135,7 +135,7 @@
|
||||
</div>
|
||||
{% 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="table-responsive">
|
||||
<table class="table align-items-center mb-0" id="{{ id }}">
|
||||
@@ -153,7 +153,16 @@
|
||||
{% for row in rows %}
|
||||
<tr>
|
||||
{% 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 %}
|
||||
{% for cell in row %}
|
||||
<td class="{{ cell.class }}">
|
||||
|
||||
@@ -21,7 +21,6 @@ def get_tools():
|
||||
|
||||
class SpecialistForm(FlaskForm):
|
||||
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
|
||||
description = TextAreaField('Description', validators=[DataRequired()])
|
||||
|
||||
retrievers = QuerySelectMultipleField(
|
||||
'Retrievers',
|
||||
@@ -37,14 +36,14 @@ class SpecialistForm(FlaskForm):
|
||||
|
||||
def __init__(self, *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
|
||||
self.type.choices = [(key, value['name']) for key, value in types_dict.items()]
|
||||
|
||||
|
||||
class EditSpecialistForm(DynamicFormBase):
|
||||
name = StringField('Name', validators=[DataRequired()])
|
||||
description = TextAreaField('Description', validators=[DataRequired()])
|
||||
description = TextAreaField('Description', validators=[Optional()])
|
||||
|
||||
retrievers = QuerySelectMultipleField(
|
||||
'Retrievers',
|
||||
@@ -55,6 +54,7 @@ class EditSpecialistForm(DynamicFormBase):
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -76,41 +76,21 @@ class BaseEditComponentForm(DynamicFormBase):
|
||||
name = StringField('Name', validators=[DataRequired()])
|
||||
description = TextAreaField('Description', validators=[Optional()])
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
role = StringField('Role', validators=[DataRequired()])
|
||||
goal = StringField('Goal', validators=[DataRequired()])
|
||||
backstory = StringField('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'
|
||||
)
|
||||
role = TextAreaField('Role', validators=[Optional()])
|
||||
goal = TextAreaField('Goal', validators=[Optional()])
|
||||
backstory = TextAreaField('Backstory', validators=[Optional()])
|
||||
|
||||
|
||||
class EveAITaskForm(BaseComponentForm):
|
||||
expected_output = TextAreaField('Expected Output', validators=[DataRequired()])
|
||||
class EditEveAITaskForm(BaseEditComponentForm):
|
||||
task_description = StringField('Task Description', validators=[Optional()])
|
||||
expected_outcome = StringField('Expected Outcome', validators=[Optional()])
|
||||
|
||||
|
||||
class EditEveAIToolForm(BaseEditComponentForm):
|
||||
pass
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import ast
|
||||
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 langchain.agents import Agent
|
||||
from sqlalchemy import desc
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
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.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 .interaction_forms import (SpecialistForm, EditSpecialistForm)
|
||||
from .interaction_forms import (SpecialistForm, EditSpecialistForm, EditEveAIAgentForm, EditEveAITaskForm,
|
||||
EditEveAIToolForm)
|
||||
|
||||
interaction_bp = Blueprint('interaction_bp', __name__, url_prefix='/interaction')
|
||||
|
||||
@@ -140,7 +143,7 @@ def specialist():
|
||||
new_specialist.name = form.name.data
|
||||
new_specialist.description = form.description.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
|
||||
|
||||
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)
|
||||
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')
|
||||
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':
|
||||
# Get the actual Retriever objects for the associated retriever_ids
|
||||
retriever_objects = Retriever.query.filter(
|
||||
Retriever.id.in_([sr.retriever_id for sr in specialist.retrievers])
|
||||
).all()
|
||||
form.retrievers.data = retriever_objects
|
||||
if specialist.description is None:
|
||||
specialist.description = specialist_config.get('metadata').get('description', '')
|
||||
|
||||
if form.validate_on_submit():
|
||||
# Update the basic fields
|
||||
@@ -230,11 +246,25 @@ def edit_specialist(specialist_id):
|
||||
db.session.rollback()
|
||||
flash(f'Failed to update specialist. Error: {str(e)}', 'danger')
|
||||
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:
|
||||
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'])
|
||||
@@ -268,3 +298,154 @@ def handle_specialist_selection():
|
||||
|
||||
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'))
|
||||
@@ -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 |
@@ -50,7 +50,7 @@ python-iso639~=2024.4.27
|
||||
python-magic~=0.4.27
|
||||
python-socketio~=5.11.3
|
||||
pytz~=2024.1
|
||||
PyYAML~=6.0.2rc1
|
||||
PyYAML~=6.0.2
|
||||
redis~=5.0.4
|
||||
requests~=2.32.3
|
||||
SQLAlchemy~=2.0.35
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
rm -f *repo.txt
|
||||
|
||||
# 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
|
||||
timestamp=$(date +"%Y-%m-%d_%H-%M")
|
||||
|
||||
@@ -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
|
||||
|
||||
# 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
|
||||
|
||||
@@ -49,4 +49,4 @@ python ${PROJECT_DIR}/scripts/initialize_data.py # Adjust the path to your init
|
||||
|
||||
# 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 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
|
||||
|
||||
@@ -13,4 +13,4 @@ chown -R appuser:appuser /app/logs
|
||||
echo "Starting EveAI Chat"
|
||||
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user