diff --git a/.repopackignore_base b/.repopackignore_base index 03a9f07..60539ff 100644 --- a/.repopackignore_base +++ b/.repopackignore_base @@ -13,6 +13,7 @@ migrations/ *material* *nucleo* *package* +*.svg nginx/mime.types *.gitignore* .python-version diff --git a/.repopackignore_eveai_app_documents b/.repopackignore_eveai_app_documents new file mode 100644 index 0000000..c0e4c77 --- /dev/null +++ b/.repopackignore_eveai_app_documents @@ -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* \ No newline at end of file diff --git a/.repopackignore_eveai_app_entitlements b/.repopackignore_eveai_app_entitlements new file mode 100644 index 0000000..705566b --- /dev/null +++ b/.repopackignore_eveai_app_entitlements @@ -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* \ No newline at end of file diff --git a/.repopackignore_eveai_app_interaction b/.repopackignore_eveai_app_interaction new file mode 100644 index 0000000..bc040ab --- /dev/null +++ b/.repopackignore_eveai_app_interaction @@ -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* \ No newline at end of file diff --git a/.repopackignore_eveai_app_user b/.repopackignore_eveai_app_user new file mode 100644 index 0000000..47d5f2a --- /dev/null +++ b/.repopackignore_eveai_app_user @@ -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* \ No newline at end of file diff --git a/common/models/interaction.py b/common/models/interaction.py index a2935d6..7084ba5 100644 --- a/common/models/interaction.py +++ b/common/models/interaction.py @@ -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) diff --git a/common/utils/cache/base.py b/common/utils/cache/base.py index d769bd2..b791fff 100644 --- a/common/utils/cache/base.py +++ b/common/utils/cache/base.py @@ -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.") diff --git a/common/utils/cache/config_cache.py b/common/utils/cache/config_cache.py index f519fb8..dcd1167 100644 --- a/common/utils/cache/config_cache.py +++ b/common/utils/cache/config_cache.py @@ -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_cache + - _version_tree_cache + - _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 + )) diff --git a/common/utils/cache/regions.py b/common/utils/cache/regions.py index 3934730..4b51149 100644 --- a/common/utils/cache/regions.py +++ b/common/utils/cache/regions.py @@ -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 diff --git a/common/utils/specialist_utils.py b/common/utils/specialist_utils.py index 8ba563b..7a56770 100644 --- a/common/utils/specialist_utils.py +++ b/common/utils/specialist_utils.py @@ -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 diff --git a/common/utils/startup_eveai.py b/common/utils/startup_eveai.py index e09af59..d23df99 100644 --- a/common/utils/startup_eveai.py +++ b/common/utils/startup_eveai.py @@ -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") diff --git a/config/config.py b/config/config.py index a558c92..eeea1ad 100644 --- a/config/config.py +++ b/config/config.py @@ -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') diff --git a/config/specialists/SPIN_SPECIALIST/1.0.0.yaml b/config/specialists/SPIN_SPECIALIST/1.0.0.yaml index 035006d..e7661b5 100644 --- a/config/specialists/SPIN_SPECIALIST/1.0.0.yaml +++ b/config/specialists/SPIN_SPECIALIST/1.0.0.yaml @@ -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" \ No newline at end of file + changes: "Initial version" + description: "A Specialist that performs both Q&A as SPIN (Sales Process) activities" \ No newline at end of file diff --git a/config/specialists/STANDARD_RAG/1.0.0.yaml b/config/specialists/STANDARD_RAG/1.0.0.yaml index 79a6e40..1fe475a 100644 --- a/config/specialists/STANDARD_RAG/1.0.0.yaml +++ b/config/specialists/STANDARD_RAG/1.0.0.yaml @@ -48,4 +48,5 @@ results: metadata: author: "Josako" date_added: "2025-01-08" - changes: "Initial version" \ No newline at end of file + changes: "Initial version" + description: "A Specialist that performs standard Q&A" \ No newline at end of file diff --git a/config/tasks/EMAIL_LEAD_DRAFTING_TASK/1.0.0.yaml b/config/tasks/EMAIL_LEAD_DRAFTING_TASK/1.0.0.yaml index 8718e7f..e39b864 100644 --- a/config/tasks/EMAIL_LEAD_DRAFTING_TASK/1.0.0.yaml +++ b/config/tasks/EMAIL_LEAD_DRAFTING_TASK/1.0.0.yaml @@ -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. diff --git a/config/tasks/EMAIL_LEAD_ENGAGEMENT_TASK/1.0.0.yaml b/config/tasks/EMAIL_LEAD_ENGAGEMENT_TASK/1.0.0.yaml index 667f022..1302c57 100644 --- a/config/tasks/EMAIL_LEAD_ENGAGEMENT_TASK/1.0.0.yaml +++ b/config/tasks/EMAIL_LEAD_ENGAGEMENT_TASK/1.0.0.yaml @@ -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. diff --git a/config/tasks/IDENTIFICATION_DETECTION_TASK/1.0.0.yaml b/config/tasks/IDENTIFICATION_DETECTION_TASK/1.0.0.yaml index c43dcf8..2f7e5c2 100644 --- a/config/tasks/IDENTIFICATION_DETECTION_TASK/1.0.0.yaml +++ b/config/tasks/IDENTIFICATION_DETECTION_TASK/1.0.0.yaml @@ -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 $) diff --git a/config/tasks/IDENTIFICATION_QUESTIONS_TASK/1.0.0.yaml b/config/tasks/IDENTIFICATION_QUESTIONS_TASK/1.0.0.yaml index 20eeeea..2634166 100644 --- a/config/tasks/IDENTIFICATION_QUESTIONS_TASK/1.0.0.yaml +++ b/config/tasks/IDENTIFICATION_QUESTIONS_TASK/1.0.0.yaml @@ -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}$$$ diff --git a/config/tasks/RAG_TASK/1.0.0.yaml b/config/tasks/RAG_TASK/1.0.0.yaml index 3191b2b..5f00b4a 100644 --- a/config/tasks/RAG_TASK/1.0.0.yaml +++ b/config/tasks/RAG_TASK/1.0.0.yaml @@ -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} diff --git a/config/tasks/SPIN_DETECT_TASK/1.0.0.yaml b/config/tasks/SPIN_DETECT_TASK/1.0.0.yaml index 144d217..5d536ad 100644 --- a/config/tasks/SPIN_DETECT_TASK/1.0.0.yaml +++ b/config/tasks/SPIN_DETECT_TASK/1.0.0.yaml @@ -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 diff --git a/config/tasks/SPIN_QUESTIONS_TASK/1.0.0.yaml b/config/tasks/SPIN_QUESTIONS_TASK/1.0.0.yaml index 4546ebc..05d12cb 100644 --- a/config/tasks/SPIN_QUESTIONS_TASK/1.0.0.yaml +++ b/config/tasks/SPIN_QUESTIONS_TASK/1.0.0.yaml @@ -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. diff --git a/docker/build_and_push_eveai.sh b/docker/build_and_push_eveai.sh index 708a52c..980d197 100755 --- a/docker/build_and_push_eveai.sh +++ b/docker/build_and_push_eveai.sh @@ -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" diff --git a/docker/copy_specialist_svgs.sh b/docker/copy_specialist_svgs.sh new file mode 100755 index 0000000..bb2d3b2 --- /dev/null +++ b/docker/copy_specialist_svgs.sh @@ -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 " + 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" \ No newline at end of file diff --git a/docker/nginx/Dockerfile b/docker/nginx/Dockerfile index 4b84791..79600e0 100644 --- a/docker/nginx/Dockerfile +++ b/docker/nginx/Dockerfile @@ -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 diff --git a/eveai_app/__init__.py b/eveai_app/__init__.py index c29ff64..754905e 100644 --- a/eveai_app/__init__.py +++ b/eveai_app/__init__.py @@ -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) + + + + + diff --git a/eveai_app/templates/interaction/component.html b/eveai_app/templates/interaction/component.html index 40cb5fb..121b8af 100644 --- a/eveai_app/templates/interaction/component.html +++ b/eveai_app/templates/interaction/component.html @@ -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.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 %} -

{{ collection_name }}

- {% 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 %} +

{{ collection_name }}

+ {% endif %} + {% for field in fields %} + {{ render_field(field, disabled_fields, exclude_fields) }} {% endfor %} - {% endif %} - -
+ {% endfor %} + {% endif %} +
+ + +
{% endblock %} {% block content_footer %} diff --git a/eveai_app/templates/interaction/components/edit_agent.html b/eveai_app/templates/interaction/components/edit_agent.html new file mode 100644 index 0000000..5b82660 --- /dev/null +++ b/eveai_app/templates/interaction/components/edit_agent.html @@ -0,0 +1 @@ +{% extends "interaction/component.html" %} diff --git a/eveai_app/templates/interaction/edit_specialist.html b/eveai_app/templates/interaction/edit_specialist.html index be80e6a..553c433 100644 --- a/eveai_app/templates/interaction/edit_specialist.html +++ b/eveai_app/templates/interaction/edit_specialist.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.hidden_tag() }} - {% set disabled_fields = ['type'] %} - {% set exclude_fields = [] %} - - {% for field in form.get_static_fields() %} - {{ render_field(field, disabled_fields, exclude_fields) }} - {% endfor %} - - {% for collection_name, fields in form.get_dynamic_fields().items() %} - {% if fields|length > 0 %} -

{{ collection_name }}

- {% endif %} - {% for field in fields %} - {{ render_field(field, disabled_fields, exclude_fields) }} - {% endfor %} - {% endfor %} - -
+
+
+ +
+
+ {{ form.hidden_tag() }} + {% set disabled_fields = ['type', 'type_version'] %} + {% set exclude_fields = [] %} + + {% for field in form.get_static_fields() %} + {{ render_field(field, disabled_fields, exclude_fields) }} + {% endfor %} + + +
+
+
+
+
+ Specialist Overview +
+
+
+
+
+ + +
+
+ + +
+ +
+ {% for collection_name, fields in form.get_dynamic_fields().items() %} + {% if fields|length > 0 %} +

{{ collection_name }}

+ {% endif %} + {% for field in fields %} + {{ render_field(field, disabled_fields, exclude_fields) }} + {% endfor %} + {% endfor %} +
+ + +
+
+
+ {{ render_selectable_table( + headers=["Agent ID", "Name", "Type", "Status"], + rows=agent_rows if agent_rows else [], + selectable=True, + id="agentsTable", + is_component_selector=True + ) }} +
+ +
+
+
+
+ + +
+
+
+ {{ render_selectable_table( + headers=["Task ID", "Name", "Type", "Status"], + rows=task_rows if task_rows else [], + selectable=True, + id="tasksTable", + is_component_selector=True + ) }} +
+ +
+
+
+
+ + +
+
+
+ {{ render_selectable_table( + headers=["Tool ID", "Name", "Type", "Status"], + rows=tool_rows if tool_rows else [], + selectable=True, + id="toolsTable", + is_component_selector=True + ) }} +
+ +
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ + +
+
+
+
{% endblock %} -{% block content_footer %} +{% block scripts %} +{{ super() }} + + + {% endblock %} + diff --git a/eveai_app/templates/interaction/specialists.html b/eveai_app/templates/interaction/specialists.html index 574362f..63d37d1 100644 --- a/eveai_app/templates/interaction/specialists.html +++ b/eveai_app/templates/interaction/specialists.html @@ -19,5 +19,5 @@ {% endblock %} {% block content_footer %} - {{ render_pagination(pagination, 'document_bp.retrievers') }} + {{ render_pagination(pagination, 'interaction_bp.specialists') }} {% endblock %} \ No newline at end of file diff --git a/eveai_app/templates/macros.html b/eveai_app/templates/macros.html index d27bc5e..d4a3039 100644 --- a/eveai_app/templates/macros.html +++ b/eveai_app/templates/macros.html @@ -135,7 +135,7 @@ {% endmacro %} -{% macro render_selectable_table(headers, rows, selectable, id) %} +{% macro render_selectable_table(headers, rows, selectable, id, is_component_selector=False) %}
@@ -153,7 +153,16 @@ {% for row in rows %} {% if selectable %} - + {% endif %} {% for cell in row %}
+ + diff --git a/eveai_app/views/interaction_forms.py b/eveai_app/views/interaction_forms.py index 69a6635..7e46762 100644 --- a/eveai_app/views/interaction_forms.py +++ b/eveai_app/views/interaction_forms.py @@ -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 + diff --git a/eveai_app/views/interaction_views.py b/eveai_app/views/interaction_views.py index 208b014..7cd9752 100644 --- a/eveai_app/views/interaction_views.py +++ b/eveai_app/views/interaction_views.py @@ -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//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//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//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//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//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//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')) \ No newline at end of file diff --git a/migrations/tenant/versions/efcd6a0d2989_add_task_description_to_eveaitask_model.py b/migrations/tenant/versions/efcd6a0d2989_add_task_description_to_eveaitask_model.py new file mode 100644 index 0000000..9639d3d --- /dev/null +++ b/migrations/tenant/versions/efcd6a0d2989_add_task_description_to_eveaitask_model.py @@ -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 ### diff --git a/nginx/static/assets/img/favicon.png b/nginx/static/assets/img/favicon.png index 08664e1..1b141ac 100644 Binary files a/nginx/static/assets/img/favicon.png and b/nginx/static/assets/img/favicon.png differ diff --git a/requirements.txt b/requirements.txt index 71c37c3..f91d2d8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/scripts/repopack_eveai.sh b/scripts/repopack_eveai.sh index 3eebe3d..3acc893 100755 --- a/scripts/repopack_eveai.sh +++ b/scripts/repopack_eveai.sh @@ -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") diff --git a/scripts/start_eveai_api.sh b/scripts/start_eveai_api.sh index d3602f5..77730a1 100755 --- a/scripts/start_eveai_api.sh +++ b/scripts/start_eveai_api.sh @@ -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 diff --git a/scripts/start_eveai_app.sh b/scripts/start_eveai_app.sh index 83cf0f3..91c7ae3 100755 --- a/scripts/start_eveai_app.sh +++ b/scripts/start_eveai_app.sh @@ -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 diff --git a/scripts/start_eveai_chat.sh b/scripts/start_eveai_chat.sh index d9ce477..f1b8939 100755 --- a/scripts/start_eveai_chat.sh +++ b/scripts/start_eveai_chat.sh @@ -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