solace-agent-mesh 0.1.3__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of solace-agent-mesh might be problematic. Click here for more details.

Files changed (45) hide show
  1. solace_agent_mesh/agents/global/actions/plantuml_diagram.py +9 -2
  2. solace_agent_mesh/agents/global/actions/plotly_graph.py +38 -40
  3. solace_agent_mesh/agents/web_request/actions/do_web_request.py +34 -33
  4. solace_agent_mesh/cli/__init__.py +1 -1
  5. solace_agent_mesh/cli/commands/add/gateway.py +162 -9
  6. solace_agent_mesh/cli/commands/build.py +0 -1
  7. solace_agent_mesh/cli/commands/init/create_other_project_files_step.py +52 -1
  8. solace_agent_mesh/cli/commands/plugin/build.py +11 -2
  9. solace_agent_mesh/cli/config.py +4 -0
  10. solace_agent_mesh/cli/utils.py +7 -2
  11. solace_agent_mesh/common/constants.py +10 -0
  12. solace_agent_mesh/common/utils.py +16 -11
  13. solace_agent_mesh/configs/service_embedding.yaml +1 -1
  14. solace_agent_mesh/configs/service_llm.yaml +1 -1
  15. solace_agent_mesh/gateway/components/gateway_base.py +7 -1
  16. solace_agent_mesh/gateway/components/gateway_input.py +8 -5
  17. solace_agent_mesh/gateway/components/gateway_output.py +12 -3
  18. solace_agent_mesh/orchestrator/components/orchestrator_stimulus_processor_component.py +23 -5
  19. solace_agent_mesh/orchestrator/orchestrator_prompt.py +155 -35
  20. solace_agent_mesh/services/file_service/file_service.py +5 -0
  21. solace_agent_mesh/services/file_service/file_service_constants.py +1 -1
  22. solace_agent_mesh/services/file_service/file_transformations.py +11 -1
  23. solace_agent_mesh/services/file_service/file_utils.py +2 -0
  24. solace_agent_mesh/services/history_service/history_providers/base_history_provider.py +21 -46
  25. solace_agent_mesh/services/history_service/history_providers/file_history_provider.py +74 -0
  26. solace_agent_mesh/services/history_service/history_providers/index.py +40 -0
  27. solace_agent_mesh/services/history_service/history_providers/memory_history_provider.py +19 -156
  28. solace_agent_mesh/services/history_service/history_providers/mongodb_history_provider.py +66 -0
  29. solace_agent_mesh/services/history_service/history_providers/redis_history_provider.py +40 -140
  30. solace_agent_mesh/services/history_service/history_providers/sql_history_provider.py +93 -0
  31. solace_agent_mesh/services/history_service/history_service.py +315 -41
  32. solace_agent_mesh/services/history_service/long_term_memory/__init__.py +0 -0
  33. solace_agent_mesh/services/history_service/long_term_memory/long_term_memory.py +399 -0
  34. solace_agent_mesh/services/llm_service/components/llm_request_component.py +19 -0
  35. solace_agent_mesh/templates/gateway-config-template.yaml +2 -1
  36. solace_agent_mesh/templates/gateway-default-config.yaml +3 -3
  37. solace_agent_mesh/templates/plugin-gateway-default-config.yaml +29 -0
  38. solace_agent_mesh/templates/rest-api-default-config.yaml +2 -1
  39. solace_agent_mesh/templates/slack-default-config.yaml +1 -1
  40. solace_agent_mesh/templates/web-default-config.yaml +2 -1
  41. {solace_agent_mesh-0.1.3.dist-info → solace_agent_mesh-0.2.0.dist-info}/METADATA +4 -3
  42. {solace_agent_mesh-0.1.3.dist-info → solace_agent_mesh-0.2.0.dist-info}/RECORD +45 -38
  43. {solace_agent_mesh-0.1.3.dist-info → solace_agent_mesh-0.2.0.dist-info}/WHEEL +0 -0
  44. {solace_agent_mesh-0.1.3.dist-info → solace_agent_mesh-0.2.0.dist-info}/entry_points.txt +0 -0
  45. {solace_agent_mesh-0.1.3.dist-info → solace_agent_mesh-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,113 +1,315 @@
1
1
  import time
2
2
  import importlib
3
- from typing import Union
3
+ import threading
4
+ from typing import Union, Tuple
4
5
 
5
6
  from solace_ai_connector.common.log import log
6
7
 
7
- from ...common.time import ONE_HOUR, FIVE_MINUTES
8
+ from ...common.time import ONE_HOUR, FIVE_MINUTES, ONE_DAY
9
+ from ...common.constants import HISTORY_MEMORY_ROLE, HISTORY_ACTION_ROLE, HISTORY_USER_ROLE, HISTORY_ASSISTANT_ROLE
8
10
  from ..common import AutoExpiry, AutoExpirySingletonMeta
9
- from .history_providers.memory_history_provider import MemoryHistoryProvider
10
- from .history_providers.redis_history_provider import RedisHistoryProvider
11
+ from .history_providers.index import HistoryProviderFactory
11
12
  from .history_providers.base_history_provider import BaseHistoryProvider
12
-
13
- HISTORY_PROVIDERS = {
14
- "redis": RedisHistoryProvider,
15
- "memory": MemoryHistoryProvider,
16
- }
17
-
13
+ from .long_term_memory.long_term_memory import LongTermMemory
18
14
 
19
15
  DEFAULT_PROVIDER = "memory"
20
16
 
21
17
  DEFAULT_MAX_TURNS = 40
22
18
  DEFAULT_MAX_CHARACTERS = 50_000
19
+ DEFAULT_SUMMARY_TIME_TO_LIVE = ONE_DAY * 5
23
20
 
24
21
  DEFAULT_HISTORY_POLICY = {
25
22
  "max_turns": DEFAULT_MAX_TURNS,
26
23
  "max_characters": DEFAULT_MAX_CHARACTERS,
27
- "enforce_alternate_message_roles": False,
24
+ "enforce_alternate_message_roles": True,
28
25
  }
29
26
 
30
27
 
31
28
  # HistoryService class - Manages history storage and retrieval
32
29
  class HistoryService(AutoExpiry, metaclass=AutoExpirySingletonMeta):
33
30
  history_provider: BaseHistoryProvider
31
+ long_term_memory_store: BaseHistoryProvider
32
+ long_term_memory_service: LongTermMemory
34
33
 
35
34
  def __init__(self, config={}, identifier=None):
35
+ """
36
+ Initialize the history service.
37
+ """
36
38
  self.identifier = identifier
37
39
  self.config = config
38
- self.provider_type = self.config.get("type", DEFAULT_PROVIDER)
39
40
  self.time_to_live = self.config.get("time_to_live", ONE_HOUR)
41
+ self.use_long_term_memory = self.config.get("enable_long_term_memory", False)
40
42
  self.expiration_check_interval = self.config.get(
41
43
  "expiration_check_interval", FIVE_MINUTES
42
44
  )
43
45
 
44
- if self.provider_type not in HISTORY_PROVIDERS and not self.config.get(
45
- "module_path"
46
- ):
47
- raise ValueError(
48
- f"Unsupported history provider type: {self.provider_type}. No module_path provided."
49
- )
50
-
51
- history_policy = {
46
+ self.history_policy = {
52
47
  **DEFAULT_HISTORY_POLICY,
53
48
  **self.config.get("history_policy", {}),
54
49
  }
55
- if self.provider_type in HISTORY_PROVIDERS:
56
- # Load built-in history provider
57
- self.history_provider = HISTORY_PROVIDERS[self.provider_type](
58
- history_policy
50
+
51
+ self.history_provider = self._get_history_provider(
52
+ self.config.get("type", DEFAULT_PROVIDER),
53
+ self.config.get("module_path"),
54
+ self.history_policy
55
+ )
56
+
57
+ if self.use_long_term_memory:
58
+ # Setting up the long-term memory service
59
+ self.long_term_memory_config = self.config.get("long_term_memory_config", {})
60
+ if not self.long_term_memory_config.get("llm_config"):
61
+ raise ValueError("Missing required configuration for Long-Term Memory provider, Missing 'model' or 'api_key' in 'history_policy.long_term_memory_config.llm_config'.")
62
+ self.long_term_memory_service = LongTermMemory(self.long_term_memory_config.get("llm_config"))
63
+
64
+ # Setting up the long-term memory store
65
+ store_config = self.long_term_memory_config.get("store_config", {})
66
+ self.long_term_memory_store = self._get_history_provider(
67
+ store_config.get("type", DEFAULT_PROVIDER),
68
+ store_config.get("module_path"),
69
+ store_config
59
70
  )
71
+
72
+ # Start the background thread for auto-expiry
73
+ self._start_auto_expiry_thread(self.expiration_check_interval)
74
+
75
+ def _get_history_provider(self, provider_type:str, module_path:str="", config:dict={}):
76
+ """
77
+ Get the history provider based on the provider type.
78
+ """
79
+ if HistoryProviderFactory.has_provider(provider_type):
80
+ return HistoryProviderFactory.get_provider_class(provider_type)(config)
60
81
  else:
82
+ if not module_path:
83
+ raise ValueError(
84
+ f"Unsupported history provider type: {provider_type}. No module_path provided."
85
+ )
61
86
  try:
62
- # Load the provider from the module path
63
87
  module_name = self.provider_type
64
- module_path = self.config.get("module_path")
65
88
  module = importlib.import_module(module_path, package=__package__)
66
89
  history_class = getattr(module, module_name)
67
90
  if not issubclass(history_class, BaseHistoryProvider):
68
91
  raise ValueError(
69
92
  f"History provider class {history_class} does not inherit from BaseHistoryProvider"
70
93
  )
71
- self.history_provider = history_class(history_policy)
94
+ return history_class(config)
72
95
  except Exception as e:
73
96
  raise ImportError("Unable to load component: " + str(e)) from e
74
97
 
75
- # Start the background thread for auto-expiry
76
- self._start_auto_expiry_thread(self.expiration_check_interval)
77
-
78
98
  def _delete_expired_items(self):
79
99
  """Checks all history entries and deletes those that have exceeded max_time_to_live."""
80
100
  current_time = time.time()
81
101
  sessions = self.history_provider.get_all_sessions()
82
102
  for session_id in sessions:
83
- session = self.history_provider.get_session_meta(session_id)
103
+ session = self.history_provider.get_session(session_id)
84
104
  if not session:
85
105
  continue
86
106
  elapsed_time = current_time - session["last_active_time"]
87
107
  if elapsed_time > self.time_to_live:
88
- self.history_provider.clear_history(session_id)
89
- log.debug(f"History for session {session_id} has expired")
108
+ self.clear_history(session_id)
109
+ log.debug("History for session %s has expired", session_id)
110
+
111
+ def _get_empty_history_entry(self):
112
+ """
113
+ Get an empty history entry.
114
+ """
115
+ return {
116
+ "history": [],
117
+ "files": [],
118
+ "summary": "",
119
+ "last_active_time": time.time(),
120
+ "num_characters": 0,
121
+ "num_turns": 0,
122
+ }
123
+
90
124
 
91
- def store_history(self, session_id: str, role: str, content: Union[str, dict]):
125
+ def _merge_assistant_with_actions(self, assistant_message:str, history:list) -> Tuple[str, list]:
126
+ """
127
+ Merge assistant message with the actions called to generate the response.
128
+ """
129
+ actions_called = []
130
+ # Looping reversely to get the most recent action
131
+ index = len(history)
132
+ for entry in reversed(history):
133
+ if entry["role"] == HISTORY_ACTION_ROLE:
134
+ actions_called.append(entry["content"])
135
+ index -= 1
136
+ else:
137
+ break
138
+ if actions_called:
139
+ actions_called_str = ""
140
+ for action_called in reversed(actions_called):
141
+ actions_called_str += (
142
+ f"\n - Agent: {action_called.get('agent_name')}"
143
+ f"\n Action: {action_called.get('action_name')}"
144
+ f"\n Action Parameters: {action_called.get('action_params')}"
145
+ )
146
+
147
+ actions_called_prompt = (
148
+ "<message_metadata>\n"
149
+ "[Following actions were called to generate this response:]"
150
+ f"{actions_called_str}"
151
+ "\n</message_metadata>\n\n"
152
+ )
153
+ assistant_message = f"{actions_called_prompt}{assistant_message}"
154
+
155
+ return assistant_message, history[:index]
156
+
157
+ def _filter_actions(self, history:list) -> list:
158
+ """
159
+ Filter out action entries from the history.
160
+ """
161
+ return [entry for entry in history if entry["role"] != HISTORY_ACTION_ROLE]
162
+
163
+ def store_history(self, session_id: str, role: str, content: Union[str, dict], other_history_props: dict = {}):
92
164
  """
93
165
  Store a new entry in the history.
94
166
 
95
167
  :param session_id: The session identifier.
96
168
  :param role: The role of the entry to be stored in the history.
97
169
  :param content: The content of the entry to be stored in the history.
170
+ :param other_history_props: Other history properties such as user identifier.
98
171
  """
99
172
  if not content:
100
173
  return
101
- return self.history_provider.store_history(session_id, role, content)
174
+
175
+ user_identity = other_history_props.get("identity", session_id)
176
+
177
+ history = self.history_provider.get_session(session_id).copy()
178
+ if not history:
179
+ history = self._get_empty_history_entry()
180
+
181
+ if role == HISTORY_ASSISTANT_ROLE:
182
+ content, history["history"] = self._merge_assistant_with_actions(content, history["history"])
183
+ elif role == HISTORY_USER_ROLE:
184
+ history["history"] = self._filter_actions(history["history"])
185
+
186
+ if (
187
+ self.history_policy.get("enforce_alternate_message_roles")
188
+ and history["num_turns"] > 0
189
+ # Check if the last entry was by the same role
190
+ and history["history"]
191
+ and history["history"][-1]["role"] == role
192
+ ):
193
+ # Append to last entry
194
+ history["history"][-1]["content"] += "\n\n" + content
195
+ else:
196
+ # Add the new entry
197
+ history["history"].append(
198
+ {"role": role, "content": content}
199
+ )
200
+ # Update the number of turns
201
+ history["num_turns"] += 1
202
+
203
+ # Update the length
204
+ history["num_characters"] += len(str(content))
205
+ # Update the last active time
206
+ history["last_active_time"] = time.time()
102
207
 
103
- def get_history(self, session_id:str) -> list:
208
+ # Extract memory from the last 2 messages if use long term memory is enabled
209
+ if self.use_long_term_memory and role == HISTORY_USER_ROLE and len(history["history"]) > 2:
210
+ recent_messages = history["history"][-3:-1].copy()
211
+ def background_task():
212
+ memory = self.long_term_memory_service.extract_memory_from_chat(recent_messages)
213
+
214
+ if memory and (memory.get("facts") or memory.get("instructions") or memory.get("update_notes")):
215
+ old_memory = self.long_term_memory_store.get_session(user_identity).get("memory", {})
216
+ updated_memory = self.long_term_memory_service.update_user_memory(old_memory, memory)
217
+ self.long_term_memory_store.update_session(user_identity, {
218
+ "memory": updated_memory
219
+ })
220
+
221
+ threading.Thread(target=background_task).start()
222
+
223
+ # Check if active session history requires truncation
224
+ if ((self.history_policy.get("max_characters") and (history["num_characters"] > self.history_policy.get("max_characters")))
225
+ or history["num_turns"] > self.history_policy.get("max_turns")):
226
+
227
+ cut_off_index = 0
228
+ if history["num_turns"] > self.history_policy.get("max_turns"):
229
+ cut_off_index = max(0, int(self.history_policy.get("max_turns") * 0.5)) # 40% of max_turns
230
+
231
+ if self.history_policy.get("max_characters") and (history["num_characters"] > self.history_policy.get("max_characters")):
232
+ index = 0
233
+ characters = 0
234
+ while characters < self.history_policy.get("max_characters") and index < len(history["history"]) - 1:
235
+ characters += len(str(history["history"][index]["content"]))
236
+ index += 1
237
+ cut_off_index = max(cut_off_index, index)
238
+
239
+ cut_off_index = min(cut_off_index, len(history["history"])) # Ensure cut_off_index is within bounds
240
+
241
+ if self.use_long_term_memory:
242
+ cut_of_history = history["history"][:cut_off_index].copy()
243
+ def background_summary_task():
244
+ summary = self.long_term_memory_service.summarize_chat(cut_of_history)
245
+ updated_summary = self.long_term_memory_service.update_summary(history["summary"], summary)
246
+
247
+ fetched_history = self.history_provider.get_session(session_id).copy()
248
+ if fetched_history:
249
+ fetched_history["summary"] = updated_summary
250
+ self.history_provider.store_session(session_id, fetched_history)
251
+
252
+ threading.Thread(target=background_summary_task).start()
253
+
254
+ history["history"] = history["history"][cut_off_index:]
255
+ history["num_characters"] = sum(len(str(entry["content"])) for entry in history["history"])
256
+ history["num_turns"] = len(history["history"])
257
+ history["last_active_time"] = time.time()
258
+
259
+ # Update the session history
260
+ return self.history_provider.store_session(session_id, history)
261
+
262
+
263
+ def get_history(self, session_id:str, other_history_props: dict = {}) -> list:
104
264
  """
105
265
  Retrieve the entire history.
106
266
 
107
267
  :param session_id: The session identifier.
268
+ :param other_history_props: Other history properties such as user identifier.
108
269
  :return: The complete history.
109
270
  """
110
- return self.history_provider.get_history(session_id)
271
+ history = self.history_provider.get_session(session_id)
272
+ messages = history.get("history", [])
273
+
274
+ if self.use_long_term_memory:
275
+ user_identity = other_history_props.get("identity", session_id)
276
+ stored_memory = self.long_term_memory_store.get_session(user_identity)
277
+ if stored_memory:
278
+ long_term_memory = self.long_term_memory_service.retrieve_user_memory(stored_memory.get("memory", {}), history.get("summary", ""))
279
+ if long_term_memory:
280
+ return [
281
+ {
282
+ "role": HISTORY_MEMORY_ROLE,
283
+ "content": long_term_memory
284
+ },
285
+ *messages
286
+ ]
287
+
288
+ return messages
289
+
290
+ def store_actions(self, session_id:str, actions:list):
291
+ """
292
+ Store an action in the history.
293
+
294
+ :param session_id: The session identifier.
295
+ :param actions: The actions to be stored in the history.
296
+ """
297
+ if not actions:
298
+ return
299
+
300
+ history = self.history_provider.get_session(session_id).copy()
301
+ if not history:
302
+ history = self._get_empty_history_entry()
303
+
304
+ for action in actions:
305
+ history["history"].append(
306
+ {"role": HISTORY_ACTION_ROLE, "content": action}
307
+ )
308
+
309
+ history["last_active_time"] = time.time()
310
+
311
+ return self.history_provider.store_session(session_id, history)
312
+
111
313
 
112
314
  def store_file(self, session_id:str, file:dict):
113
315
  """
@@ -118,7 +320,20 @@ class HistoryService(AutoExpiry, metaclass=AutoExpirySingletonMeta):
118
320
  """
119
321
  if not file:
120
322
  return
121
- return self.history_provider.store_file(session_id, file)
323
+
324
+ history = self.history_provider.get_session(session_id).copy()
325
+ if not history:
326
+ history = self._get_empty_history_entry()
327
+
328
+ # Check duplicate
329
+ for f in history["files"]:
330
+ if f.get("url") and f.get("url") == file.get("url"):
331
+ return
332
+
333
+ history["files"].append(file)
334
+ history["last_active_time"] = time.time()
335
+
336
+ return self.history_provider.store_session(session_id, history)
122
337
 
123
338
  def get_files(self, session_id:str) -> list:
124
339
  """
@@ -127,13 +342,72 @@ class HistoryService(AutoExpiry, metaclass=AutoExpirySingletonMeta):
127
342
  :param session_id: The session identifier.
128
343
  :return: The files for the session.
129
344
  """
130
- return self.history_provider.get_files(session_id)
345
+ history = self.history_provider.get_session(session_id)
346
+ if not history:
347
+ return []
348
+
349
+ files = []
350
+ current_time = time.time()
351
+ all_files = history["files"].copy()
352
+
353
+ for file in all_files:
354
+ expiration_timestamp = file.get("expiration_timestamp")
355
+ if expiration_timestamp and current_time > expiration_timestamp:
356
+ history["files"].remove(file)
357
+ continue
358
+ files.append(file)
359
+
360
+ self.history_provider.store_session(session_id, history)
361
+ return files
131
362
 
132
- def clear_history(self, session_id:str, keep_levels=0):
363
+
364
+ def clear_history(self, session_id:str, keep_levels=0, clear_files=True):
133
365
  """
134
366
  Clear the history and files, optionally keeping a specified number of recent entries.
135
367
 
136
368
  :param session_id: The session identifier.
137
369
  :param keep_levels: Number of most recent history entries to keep. Default is 0 (clear all).
370
+ :param other_history_props: Other history properties such as user identifier.
371
+ :param clear_files: Whether to clear associated files. Default is True.
138
372
  """
139
- return self.history_provider.clear_history(session_id, keep_levels, clear_files=True)
373
+
374
+ history = self.history_provider.get_session(session_id).copy()
375
+ if not history:
376
+ return
377
+
378
+ if history.get("history") or (clear_files and history.get("files")):
379
+ cut_off_index = max(0, len(history["history"]) - keep_levels)
380
+ cut_off_history = history["history"][:cut_off_index]
381
+
382
+ if self.use_long_term_memory and cut_off_history:
383
+ def background_task():
384
+ summary = self.long_term_memory_service.summarize_chat(cut_off_history)
385
+ updated_summary = self.long_term_memory_service.update_summary(history["summary"], summary)
386
+
387
+ fetched_history = self.history_provider.get_session(session_id).copy()
388
+ if fetched_history:
389
+ fetched_history["summary"] = updated_summary
390
+ self.history_provider.store_session(session_id, fetched_history)
391
+ threading.Thread(target=background_task).start()
392
+
393
+ history["history"] = [] if keep_levels <= 0 else history["history"][-keep_levels:]
394
+ history["num_turns"] = keep_levels
395
+ history["num_characters"] = sum(len(str(entry["content"])) for entry in history["history"])
396
+ history["last_active_time"] = time.time()
397
+
398
+ if clear_files:
399
+ history["files"] = []
400
+
401
+ return self.history_provider.store_session(session_id, history)
402
+
403
+ # Summaries get cleared at a longer expiry time
404
+ elif self.use_long_term_memory and history.get("summary"):
405
+ elapsed_time = time.time() - history["last_active_time"]
406
+ summary_ttl = self.long_term_memory_config.get("summary_time_to_live", DEFAULT_SUMMARY_TIME_TO_LIVE)
407
+ if elapsed_time > summary_ttl:
408
+ return self.history_provider.delete_session(session_id)
409
+
410
+ # Delete the session if it has no chat history, files or summary
411
+ else:
412
+ return self.history_provider.delete_session(session_id)
413
+