11import asyncio
2+ import json
23import operator
34import threading
4- from configparser import ConfigParser
55from datetime import datetime
6+ from pathlib import Path
67from typing import Any , Callable , get_type_hints
78
9+ import yaml
10+
811from besser .agent .core .transition .event import Event
912from besser .agent .core .message import Message , MessageType
1013from besser .agent .core .entity .entity import Entity
1619from besser .agent .core .state import State
1720from besser .agent .core .transition .transition import Transition
1821from besser .agent .db import DB_MONITORING
22+ from besser .agent .db .db_handler import DBHandler
1923from besser .agent .db .monitoring_db import MonitoringDB
2024from besser .agent .exceptions .exceptions import AgentNotTrainedError , DuplicatedEntityError , DuplicatedInitialStateError , \
2125 DuplicatedIntentError , DuplicatedStateError , InitialStateNotFound
@@ -48,7 +52,7 @@ class Agent:
4852 _event_loop (asyncio.AbstractEventLoop): The event loop managing external events
4953 _event_thread (threading.Thread): The thread where the event loop is run
5054 _nlp_engine (NLPEngine): The agent NLP engine
51- _config (ConfigParser ): The agent configuration parameters
55+ _config (dict[str, Any] ): The agent configuration parameters
5256 _default_ic_config (IntentClassifierConfiguration): the intent classifier configuration used by default for the
5357 agent states
5458 _sessions (dict[str, Session]): The agent sessions
@@ -64,23 +68,38 @@ class Agent:
6468 processors (list[Processors]): List of processors used by the agent
6569 """
6670
67- def __init__ (self , name : str , persist_sessions : bool = False ):
71+ def __init__ (
72+ self ,
73+ name : str ,
74+ persist_sessions : bool = False ,
75+ user_profiles_path : str | None = None ,
76+ agent_configurations_path : str | None = None ,
77+ ):
6878 self ._name : str = name
6979 self ._persist_sessions : bool = persist_sessions
7080 self ._platforms : list [Platform ] = []
7181 self ._platforms_threads : list [threading .Thread ] = []
7282 self ._nlp_engine = NLPEngine (self )
73- self ._config : ConfigParser = ConfigParser ()
83+ self ._config : dict [ str , Any ] = {}
7484 self ._default_ic_config : IntentClassifierConfiguration = SimpleIntentClassifierConfiguration ()
7585 self ._sessions : dict [str , Session ] = {}
7686 self ._trained : bool = False
7787 self ._monitoring_db : MonitoringDB = None
88+ self ._db_handler : DBHandler | None = None
7889 self .states : list [State ] = []
7990 self .intents : list [Intent ] = []
8091 self .entities : list [Entity ] = []
8192 self .global_initial_states : list [tuple [State , Intent ]] = []
8293 self .global_state_component : dict [State , list [State ]] = dict ()
8394 self .processors : list [Processor ] = []
95+ self ._user_profiles : Any = None
96+ self ._agent_configurations : dict [str , Any ] = {}
97+
98+ if user_profiles_path :
99+ self .load_user_profiles (user_profiles_path )
100+
101+ if agent_configurations_path :
102+ self .load_agent_configurations (agent_configurations_path )
84103
85104 @property
86105 def name (self ):
@@ -94,20 +113,72 @@ def nlp_engine(self):
94113
95114 @property
96115 def config (self ):
97- """ConfigParser : The agent configuration parameters."""
116+ """dict[str, Any] : The agent configuration parameters."""
98117 return self ._config
99118
100119 def load_properties (self , path : str ) -> None :
101120 """Read a properties file and store its properties in the agent configuration.
102121
103- An example properties file , `config.ini`:
122+ Supported formats are YAML (``.yaml`` , ``.yml``).
104123
105- .. literalinclude:: ../../../../besser/agent/test/examples/config.ini
124+ Example YAML properties file, ``config.yaml``:
125+
126+ .. literalinclude:: ../../../../besser/agent/test/examples/config.yaml
106127
107128 Args:
108129 path (str): the path to the properties file
109130 """
110- self ._config .read (path )
131+ suffix = Path (path ).suffix .lower ()
132+ if suffix not in {'.yaml' , '.yml' }:
133+ raise ValueError ('Only YAML configuration files are supported (.yaml, .yml)' )
134+
135+ with open (path , encoding = 'utf-8' ) as config_file :
136+ loaded_config = yaml .safe_load (config_file ) or {}
137+ if not isinstance (loaded_config , dict ):
138+ raise ValueError ('YAML properties file must contain a mapping at the root level' )
139+ self ._flatten_yaml_properties (loaded_config )
140+
141+ def _flatten_yaml_properties (self , data : Any , prefix : str = '' ) -> None :
142+ if isinstance (data , dict ):
143+ for key , value in data .items ():
144+ next_prefix = f'{ prefix } .{ key } ' if prefix else str (key )
145+ self ._flatten_yaml_properties (value , next_prefix )
146+ return
147+
148+ if isinstance (data , list ):
149+ for index , item in enumerate (data ):
150+ if isinstance (item , dict ) and len (item ) == 1 :
151+ item_key , item_value = next (iter (item .items ()))
152+ next_prefix = f'{ prefix } .{ item_key } ' if prefix else str (item_key )
153+ self ._flatten_yaml_properties (item_value , next_prefix )
154+ else :
155+ next_prefix = f'{ prefix } .{ index } ' if prefix else str (index )
156+ self ._flatten_yaml_properties (item , next_prefix )
157+ return
158+
159+ if prefix :
160+ self ._config [prefix ] = data
161+
162+ @staticmethod
163+ def _coerce_property_value (value : Any , target_type : type ) -> Any :
164+ if isinstance (value , target_type ):
165+ return value
166+
167+ if target_type is bool :
168+ if isinstance (value , str ):
169+ normalized = value .strip ().lower ()
170+ if normalized in {'true' , '1' , 'yes' , 'y' , 'on' }:
171+ return True
172+ if normalized in {'false' , '0' , 'no' , 'n' , 'off' }:
173+ return False
174+ raise ValueError (f'Cannot parse boolean value from { value !r} ' )
175+ if isinstance (value , (int , float )):
176+ return bool (value )
177+
178+ if target_type in {str , int , float }:
179+ return target_type (value )
180+
181+ return value
111182
112183 def get_property (self , prop : Property ) -> Any :
113184 """Get an agent property's value
@@ -118,17 +189,22 @@ def get_property(self, prop: Property) -> Any:
118189 Returns:
119190 Any: the property value, or None
120191 """
121- if prop .type == str :
122- getter = self ._config .get
123- elif prop .type == bool :
124- getter = self ._config .getboolean
125- elif prop .type == int :
126- getter = self ._config .getint
127- elif prop .type == float :
128- getter = self ._config .getfloat
129- else :
130- return None
131- return getter (prop .section , prop .name , fallback = prop .default_value )
192+ value = self ._config .get (prop .name )
193+
194+ if value is None :
195+ return prop .default_value
196+
197+ try :
198+ return self ._coerce_property_value (value , prop .type )
199+ except (TypeError , ValueError ):
200+ logger .warning (
201+ "Could not cast property '%s' value %r to %s. Using default value %r" ,
202+ prop .name ,
203+ value ,
204+ prop .type ,
205+ prop .default_value ,
206+ )
207+ return prop .default_value
132208
133209 def set_property (self , prop : Property , value : Any ):
134210 """Set an agent property.
@@ -138,11 +214,71 @@ def set_property(self, prop: Property, value: Any):
138214 value (Any): the property value
139215 """
140216 if (value is not None ) and (not isinstance (value , prop .type )):
141- raise TypeError (f"Attempting to set the agent property '{ prop .name } ' in section ' { prop . section } ' with a "
217+ raise TypeError (f"Attempting to set the agent property '{ prop .name } ' with a "
142218 f"{ type (value )} value: { value } . The expected property value type is { prop .type } " )
143- if prop .section not in self ._config .sections ():
144- self ._config .add_section (prop .section )
145- self ._config .set (prop .section , prop .name , str (value ))
219+ self ._config [prop .name ] = value
220+
221+ @property
222+ def user_profiles (self ) -> Any :
223+ """Return loaded user profiles data, or None if not set."""
224+ return self ._user_profiles
225+
226+ def load_user_profiles (self , path : str ) -> None :
227+ """Load user profiles from a JSON file and store data."""
228+ try :
229+ with open (path , encoding = 'utf-8' ) as profiles_file :
230+ self ._user_profiles = json .load (profiles_file )
231+ except FileNotFoundError :
232+ logger .error ("User profiles file not found at %s" , path )
233+ self ._user_profiles = None
234+ except json .JSONDecodeError :
235+ logger .error ("Failed to parse user profiles JSON at %s" , path )
236+ self ._user_profiles = None
237+
238+ def set_user_profiles (self , profiles : Any ) -> None :
239+ """Set user profiles programmatically."""
240+ self ._user_profiles = profiles
241+
242+ @property
243+ def agent_configurations (self ) -> dict [str , Any ]:
244+ """Return loaded agent configurations mapped by profile/user key."""
245+ return self ._agent_configurations
246+
247+ def load_agent_configurations (self , path : str ) -> None :
248+ """Load agent configurations from a JSON file.
249+
250+ Expected format: a mapping where keys represent profile/user identifiers
251+ and values are the corresponding agent configuration objects.
252+ """
253+ try :
254+ with open (path , encoding = 'utf-8' ) as config_file :
255+ data = json .load (config_file )
256+
257+ if isinstance (data , dict ):
258+ self ._agent_configurations = data
259+ else :
260+ logger .error ("Agent configurations JSON at %s must be an object mapping keys to configurations" , path )
261+ self ._agent_configurations = {}
262+ except FileNotFoundError :
263+ logger .error ("Agent configurations file not found at %s" , path )
264+ self ._agent_configurations = {}
265+ except json .JSONDecodeError :
266+ logger .error ("Failed to parse agent configurations JSON at %s" , path )
267+ self ._agent_configurations = {}
268+
269+ def set_agent_configurations (self , configurations : dict [str , Any ] | None ) -> None :
270+ """Set agent configurations programmatically.
271+
272+ Args:
273+ configurations (dict[str, Any] | None): mapping of profile/user keys to config objects.
274+ """
275+ if configurations is None :
276+ self ._agent_configurations = {}
277+ return
278+
279+ if not isinstance (configurations , dict ):
280+ raise TypeError ("Agent configurations must be a dictionary mapping keys to configuration objects" )
281+ self ._agent_configurations = configurations
146282
147283 def set_default_ic_config (self , ic_config : IntentClassifierConfiguration ):
148284 """Set the default intent classifier configuration.
@@ -347,6 +483,8 @@ def stop(self) -> None:
347483 self ._stop_platforms ()
348484 if self .get_property (DB_MONITORING ) and self ._monitoring_db .connected :
349485 self ._monitoring_db .close_connection ()
486+ if self ._db_handler is not None :
487+ self ._db_handler .close_all ()
350488
351489 for session_id in list (self ._sessions .keys ()):
352490 self .close_session (session_id )
@@ -526,7 +664,11 @@ def delete_session(self, session_id: str) -> None:
526664 self ._monitoring_db_delete_session (self ._sessions [session_id ])
527665 del self ._sessions [session_id ]
528666
529- def use_websocket_platform (self , use_ui : bool = True , authenticate_users : bool = False ) -> WebSocketPlatform :
667+ def use_websocket_platform (
668+ self ,
669+ use_ui : bool = True ,
670+ authenticate_users : bool = False ,
671+ ) -> WebSocketPlatform :
530672 """Use the :class:`~besser.agent.platforms.websocket.websocket_platform.WebSocketPlatform` on this agent.
531673
532674 Args:
@@ -536,7 +678,11 @@ def use_websocket_platform(self, use_ui: bool = True, authenticate_users: bool =
536678 Returns:
537679 WebSocketPlatform: the websocket platform
538680 """
539- websocket_platform = WebSocketPlatform (self , use_ui , authenticate_users )
681+ websocket_platform = WebSocketPlatform (
682+ self ,
683+ use_ui ,
684+ authenticate_users ,
685+ )
540686 self ._platforms .append (websocket_platform )
541687 return websocket_platform
542688
@@ -580,6 +726,18 @@ def use_a2a_platform(self) -> A2APlatform:
580726 self ._platforms .append (a2a_platform )
581727 return a2a_platform
582728
729+ def use_db_handler (self ) -> DBHandler :
730+ """Use the :class:`~besser.agent.db.db_handler.DBHandler` on this agent.
731+
732+ DB connections are established lazily, when the first query is executed.
733+
734+ Returns:
735+ DBHandler: the database handler
736+ """
737+ if self ._db_handler is None :
738+ self ._db_handler = DBHandler (self )
739+ return self ._db_handler
740+
583741 def _monitoring_db_insert_session (self , session : Session ) -> None :
584742 """Insert a session record into the monitoring database.
585743
0 commit comments