Skip to content

Commit 3e6c61e

Browse files
committed
2 parents 76c7959 + d06c77e commit 3e6c61e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+2885
-210
lines changed

besser/agent/__init__.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22

33
from besser.agent.core.property import Property
44

5-
SECTION_AGENT = 'agent'
6-
7-
CHECK_TRANSITIONS_DELAY = Property(SECTION_AGENT, 'agent.check_transitions.delay', float, 1.0)
5+
CHECK_TRANSITIONS_DELAY = Property('agent.check_transitions_delay', float, 1.0)
86
"""
97
An agent evaluates periodically all the transitions from the current state to check if someone is satisfied.
108

besser/agent/core/agent.py

Lines changed: 183 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import asyncio
2+
import json
23
import operator
34
import threading
4-
from configparser import ConfigParser
55
from datetime import datetime
6+
from pathlib import Path
67
from typing import Any, Callable, get_type_hints
78

9+
import yaml
10+
811
from besser.agent.core.transition.event import Event
912
from besser.agent.core.message import Message, MessageType
1013
from besser.agent.core.entity.entity import Entity
@@ -16,6 +19,7 @@
1619
from besser.agent.core.state import State
1720
from besser.agent.core.transition.transition import Transition
1821
from besser.agent.db import DB_MONITORING
22+
from besser.agent.db.db_handler import DBHandler
1923
from besser.agent.db.monitoring_db import MonitoringDB
2024
from 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

besser/agent/core/property.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,20 @@ class Property:
55
"""An agent property.
66
77
Args:
8-
section (str): the property section
98
name (str): the property name
109
property_type (type): the property type
1110
default_value (Any): the property default value
1211
1312
Attributes:
14-
section (str): The property section
1513
name (str): The property name
1614
type (type): The property type
1715
default_value (Any): The property default value
1816
"""
19-
def __init__(self, section: str, name: str, property_type: type, default_value: Any):
17+
def __init__(self, name: str, property_type: type, default_value: Any):
2018
if (default_value is not None) and (not isinstance(default_value, property_type)):
21-
raise TypeError(f"Attempting to create a property '{name}' in section '{section}' with a "
19+
raise TypeError(f"Attempting to create a property '{name}' with a "
2220
f"{type(default_value)} default value: {default_value}. The expected property value type "
2321
f"is {property_type}")
24-
self.section = section
2522
self.name = name
2623
self.type = property_type
2724
self.default_value = default_value

besser/agent/core/session.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
if TYPE_CHECKING:
2525
from besser.agent.core.agent import Agent
26+
from besser.agent.db.db_handler import DBHandler
2627
from besser.agent.core.state import State
2728
from besser.agent.platforms.platform import Platform
2829

@@ -82,6 +83,11 @@ def platform(self):
8283
"""Platform: The session platform."""
8384
return self._platform
8485

86+
@property
87+
def db_handler(self) -> 'DBHandler':
88+
"""DBHandler: relational DB handler configured for this agent."""
89+
return self._agent.use_db_handler()
90+
8591
@property
8692
def current_state(self):
8793
"""State: The current agent state of the session."""

0 commit comments

Comments
 (0)