solace-agent-mesh 0.1.2__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 (59) hide show
  1. solace_agent_mesh/agents/base_agent_component.py +2 -0
  2. solace_agent_mesh/agents/global/actions/plantuml_diagram.py +14 -2
  3. solace_agent_mesh/agents/global/actions/plotly_graph.py +49 -40
  4. solace_agent_mesh/agents/web_request/actions/do_web_request.py +34 -33
  5. solace_agent_mesh/cli/__init__.py +1 -1
  6. solace_agent_mesh/cli/commands/add/gateway.py +162 -9
  7. solace_agent_mesh/cli/commands/build.py +0 -1
  8. solace_agent_mesh/cli/commands/init/builtin_agent_step.py +1 -6
  9. solace_agent_mesh/cli/commands/init/create_config_file_step.py +5 -0
  10. solace_agent_mesh/cli/commands/init/create_other_project_files_step.py +52 -1
  11. solace_agent_mesh/cli/commands/init/init.py +1 -5
  12. solace_agent_mesh/cli/commands/init/project_structure_step.py +0 -29
  13. solace_agent_mesh/cli/commands/plugin/add.py +3 -1
  14. solace_agent_mesh/cli/commands/plugin/build.py +11 -2
  15. solace_agent_mesh/cli/commands/plugin/plugin.py +20 -5
  16. solace_agent_mesh/cli/commands/plugin/remove.py +3 -1
  17. solace_agent_mesh/cli/config.py +4 -0
  18. solace_agent_mesh/cli/utils.py +7 -2
  19. solace_agent_mesh/common/action_response.py +13 -0
  20. solace_agent_mesh/common/constants.py +12 -0
  21. solace_agent_mesh/common/postgres_database.py +11 -5
  22. solace_agent_mesh/common/utils.py +16 -11
  23. solace_agent_mesh/configs/monitor_stim_and_errors_to_slack.yaml +3 -0
  24. solace_agent_mesh/configs/service_embedding.yaml +1 -1
  25. solace_agent_mesh/configs/service_llm.yaml +1 -1
  26. solace_agent_mesh/gateway/components/gateway_base.py +7 -1
  27. solace_agent_mesh/gateway/components/gateway_input.py +8 -5
  28. solace_agent_mesh/gateway/components/gateway_output.py +12 -3
  29. solace_agent_mesh/orchestrator/action_manager.py +13 -1
  30. solace_agent_mesh/orchestrator/components/orchestrator_stimulus_processor_component.py +25 -5
  31. solace_agent_mesh/orchestrator/orchestrator_prompt.py +155 -35
  32. solace_agent_mesh/services/file_service/file_service.py +5 -0
  33. solace_agent_mesh/services/file_service/file_service_constants.py +1 -1
  34. solace_agent_mesh/services/file_service/file_transformations.py +11 -1
  35. solace_agent_mesh/services/file_service/file_utils.py +2 -0
  36. solace_agent_mesh/services/history_service/history_providers/base_history_provider.py +21 -45
  37. solace_agent_mesh/services/history_service/history_providers/file_history_provider.py +74 -0
  38. solace_agent_mesh/services/history_service/history_providers/index.py +40 -0
  39. solace_agent_mesh/services/history_service/history_providers/memory_history_provider.py +19 -153
  40. solace_agent_mesh/services/history_service/history_providers/mongodb_history_provider.py +66 -0
  41. solace_agent_mesh/services/history_service/history_providers/redis_history_provider.py +40 -137
  42. solace_agent_mesh/services/history_service/history_providers/sql_history_provider.py +93 -0
  43. solace_agent_mesh/services/history_service/history_service.py +315 -41
  44. solace_agent_mesh/services/history_service/long_term_memory/__init__.py +0 -0
  45. solace_agent_mesh/services/history_service/long_term_memory/long_term_memory.py +399 -0
  46. solace_agent_mesh/services/llm_service/components/llm_request_component.py +24 -0
  47. solace_agent_mesh/templates/gateway-config-template.yaml +2 -1
  48. solace_agent_mesh/templates/gateway-default-config.yaml +3 -3
  49. solace_agent_mesh/templates/plugin-gateway-default-config.yaml +29 -0
  50. solace_agent_mesh/templates/rest-api-default-config.yaml +2 -1
  51. solace_agent_mesh/templates/slack-default-config.yaml +1 -1
  52. solace_agent_mesh/templates/web-default-config.yaml +2 -1
  53. {solace_agent_mesh-0.1.2.dist-info → solace_agent_mesh-0.2.0.dist-info}/METADATA +38 -8
  54. {solace_agent_mesh-0.1.2.dist-info → solace_agent_mesh-0.2.0.dist-info}/RECORD +57 -52
  55. solace_agent_mesh/cli/commands/init/rest_api_step.py +0 -50
  56. solace_agent_mesh/cli/commands/init/web_ui_step.py +0 -40
  57. {solace_agent_mesh-0.1.2.dist-info → solace_agent_mesh-0.2.0.dist-info}/WHEEL +0 -0
  58. {solace_agent_mesh-0.1.2.dist-info → solace_agent_mesh-0.2.0.dist-info}/entry_points.txt +0 -0
  59. {solace_agent_mesh-0.1.2.dist-info → solace_agent_mesh-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -2,42 +2,39 @@ from typing import Dict, Any, List
2
2
  import yaml
3
3
  from langchain_core.messages import HumanMessage
4
4
  from ..services.file_service import FS_PROTOCOL, Types, LLM_QUERY_OPTIONS, TRANSFORMERS
5
+ from solace_ai_connector.common.log import log
5
6
 
6
7
  # Cap the number of examples so we don't overwhelm the LLM
7
8
  MAX_SYSTEM_PROMPT_EXAMPLES = 6
8
9
 
9
10
  # Examples that should always be included in the prompt
10
11
  fixed_examples = [
11
- """ <example>
12
- <example_docstring>
13
- This example shows a stimulus from a chatbot gateway in which a user is asking about the top stories on the website hacker news. The web_request is not yet open, so the change_agent_status action is invoked to open the web_request agent.
14
- </example_docstring>
15
- <example_stimulus>
16
- <{tp}stimulus starting_id="1"/>
17
- What is the top story on hacker news?
18
- </{tp}stimulus>
19
- <{tp}stimulus_metadata>
20
- local_time: 2024-11-06 15:58:04 EST-0500 (Wednesday)
21
- </{tp}stimulus_metadata>
22
- </example_stimulus>
23
- <example_response>
24
- <{tp}reasoning>
25
- - User is asking for the top story on Hacker News
26
- - We need to use the web_request agent to fetch the latest information
27
- - The web_request agent is currently closed, so we need to open it first
28
- - After opening the agent, we\'ll need to make a web request to Hacker News
29
- </{tp}reasoning>
30
-
31
- <{tp}status_update>To get the latest top story from Hacker News, I\'ll need to access the web. I\'m preparing to do that now.</{tp}status_update>
32
-
33
- <{tp}invoke_action agent="global" action="change_agent_status">
34
- <{tp}parameter name="agent_name">web_request</{tp}parameter>
35
- <{tp}parameter name="new_state">open</{tp}parameter>
36
- </{tp}invoke_action>
37
- </example_response>
38
- </example>
39
- """
40
- ]
12
+ {
13
+ "docstring": "This example shows a stimulus from a chatbot gateway in which a user is asking about the top stories on the website hacker news. The web_request is not yet open, so the change_agent_status action is invoked to open the web_request agent.",
14
+ "tag_prefix_placeholder": "{tp}",
15
+ "starting_id": "1",
16
+ "user_input": "What is the top story on hacker news?",
17
+ "metadata": [
18
+ "local_time: 2024-11-06 15:58:04 EST-0500 (Wednesday)"
19
+ ],
20
+ "reasoning": [
21
+ "- User is asking for the top story on Hacker News",
22
+ "- We need to use the web_request agent to fetch the latest information",
23
+ "- The web_request agent is currently closed, so we need to open it first",
24
+ "- After opening the agent, we'll need to make a web request to Hacker News"
25
+ ],
26
+ "response_text": "",
27
+ "status_update": "To get the latest top story from Hacker News, I'll need to access the web. I'm preparing to do that now.",
28
+ "action": {
29
+ "agent": "global",
30
+ "name": "change_agent_status",
31
+ "parameters": {
32
+ "agent_name": "web_request",
33
+ "new_state": "open"
34
+ }
35
+ }
36
+ }
37
+ ]
41
38
 
42
39
 
43
40
  def get_file_handling_prompt(tp: str) -> str:
@@ -102,6 +99,7 @@ def get_file_handling_prompt(tp: str) -> str:
102
99
  For all files, there's `encoding` parameter, this is used to encode the file format. The supported values are `datauri`, `base64`, `zip`, and `gzip`. Use this to convert a file to ZIP or datauri for HTML encoding.
103
100
  For example (return a file as zip): `{FS_PROTOCOL}://c27e6908-55d5-4ce0-bc93-a8e28f84be12_annual_report.csv?encoding=zip&resolve=true`
104
101
  Example 2 (HTML tag must be datauri encoded): `<img src="{FS_PROTOCOL}://c9183b0f-fd11-48b4-ad8f-df221bff3da9_generated_image.png?encoding=datauri&resolve=true" alt="image">`
102
+ Example 3 (return a regular image directly): `amfs://a1b2c3d4-5e6f-7g8h-9i0j-1k2l3m4n5o6p_photo.png?resolve=true` - When returning images directly in messaging platforms like Slack, don't use any encoding parameter
105
103
 
106
104
  For all files, there's `resolve=true` parameter, this is used to resolve the url to the actual data before processing.
107
105
  For example: `{FS_PROTOCOL}://577c928c-c126-42d8-8a48-020b93096110_names.csv?resolve=true`
@@ -156,11 +154,15 @@ def create_examples(
156
154
  String containing all examples with replaced placeholders
157
155
  """
158
156
  examples = (fixed_examples + agent_examples)[:MAX_SYSTEM_PROMPT_EXAMPLES]
159
- return "\n".join([example.replace("{tp}", tp) for example in examples])
157
+ formatted_examples = format_examples_by_llm_type(examples)
158
+
159
+ return "\n".join([example.replace("{tp}", tp) for example in formatted_examples])
160
160
 
161
161
 
162
162
  def SystemPrompt(info: Dict[str, Any], action_examples: List[str]) -> str:
163
- response_format_prompt = info.get("response_format_prompt", "")
163
+ tp = info["tag_prefix"]
164
+ response_format_prompt = info.get("response_format_prompt", "") or ""
165
+ response_format_prompt = response_format_prompt.replace("{{tag_prefix}}", tp)
164
166
  response_guidelines_prompt = (
165
167
  f"<response_guidelines>\nConsider the following when generating a response to the originator:\n"
166
168
  f"{response_format_prompt}</response_guidelines>"
@@ -183,7 +185,6 @@ def SystemPrompt(info: Dict[str, Any], action_examples: List[str]) -> str:
183
185
  )
184
186
 
185
187
  # Merged
186
- tp = info["tag_prefix"]
187
188
  examples = create_examples(fixed_examples, action_examples, tp)
188
189
 
189
190
  handling_files = get_file_handling_prompt(tp)
@@ -216,7 +217,21 @@ The assistant's behavior aligns with the system purpose specified below:
216
217
  5. Do not perform any other actions besides opening the required agents in this step.
217
218
  - Report generation:
218
219
  1. If a report is requested and no format is specified, create the report in an HTML file.
219
- 2. Generate each section of the report independently and store it in the file service with create_file action. When finishing the report, combine the sections using amfs urls with the resolve=true query parameter to insert the sections into the main document. When generating HTML, create the header first with all the necessary CSS and JS links so that it is clear what css the rest of the document will use.
220
+ 2. Generate each section of the report independently and store it in the file service with create_file action. When finishing the report, combine the sections using amfs urls with the resolve=true query parameter to insert the sections into the main document. When inserting amfs HTML URLs into the HTML document, place them directly in the document without any surrounding tags or brackets. Here is an example of the body section of an HTML report combining multiple sections:
221
+ <body>
222
+ <!-- Title -->
223
+ <h1>Report Title</h1>
224
+
225
+ <!-- Section 1 -->
226
+ amfs://xxxxxx.html?resolve=true
227
+
228
+ <!-- Section 2 -->
229
+ amfs://yyyyyy.html?resolve=true
230
+
231
+ <!-- Section 3 -->
232
+ amfs://zzzzzz.html?resolve=true
233
+ </body>
234
+ When generating HTML, create the header first with all the necessary CSS and JS links so that it is clear what css the rest of the document will use.
220
235
  3. Images are always very useful in reports, so the assistant will add them when appropriate. If images are embedded in html, they must be resolved and converted to datauri format or they won't render in the final document. This can be done by using the encoding=datauri&resolve=true in the amfs link. For example, <img src="amfs://xxxxxx.png?encoding=datauri&resolve=true". The assistant will take care of the rest. Images can be created in parallel
221
236
  4. During report generation in interactive sessions, the assistant will send lots of status messages to indicate what is happening.
222
237
  - Handling stimuli with open agents:
@@ -330,7 +345,7 @@ def UserStimulusPrompt(
330
345
  f"\tExample of returning a file as zip: <{info['tag_prefix']}file><url>{FS_PROTOCOL}://519321d8-3506-4f8d-9377-e5d6ce74d917_filename.csv?encoding=zip</url></{info['tag_prefix']}file>.\n"
331
346
  f"You can also optionally return a non-persistent temporary file using the format <{info['tag_prefix']}file name=\"filename.csv\" mime_type=\"text/csv\">\n<data> data </data>\n</{info['tag_prefix']}file>.\n"
332
347
  f" can't nest `<{info['tag_prefix']}file>` tags inside the `<data>` tag. If you need to address another file within the data, use the URL with the `resolve=true` query parameter.\n"
333
- "When using a {FS_PROTOCOL} URL outside of a file block, always include the 'resolve=true' query parameter to ensure the URL is resolved to the actual data.\n"
348
+ f"When using a {FS_PROTOCOL} URL outside of a file block, always include the 'resolve=true' query parameter to ensure the URL is resolved to the actual data.\n"
334
349
  f"</{info['tag_prefix']}file_instructions>"
335
350
  )
336
351
 
@@ -413,3 +428,108 @@ you should ask the originator for more information. Include links to the source
413
428
  {context}
414
429
  </context>
415
430
  """
431
+
432
+
433
+ def format_examples_by_llm_type(examples: list, llm_type: str = "anthropic") -> list:
434
+ """
435
+ Render examples based on llm type
436
+
437
+ Args:
438
+ llm_type (str): The type of LLM to render examples for (default: "anthropic")
439
+ examples (list): List of examples in model-agnostic format
440
+
441
+ Returns:
442
+ list: List of examples formatted for the specified LLM
443
+ """
444
+ formatted_examples = []
445
+
446
+ if llm_type == "anthropic":
447
+ for example in examples:
448
+ formatted_example = format_example_for_anthropic(example)
449
+ formatted_examples.append(formatted_example)
450
+ else:
451
+ log.error(f"Unsupported LLM type: {llm_type}")
452
+
453
+ return formatted_examples
454
+
455
+ def format_example_for_anthropic(example: dict) -> str:
456
+ """
457
+ Format an example for the Anthropic's LLMs
458
+ """
459
+
460
+ tag_prefix = example.get("tag_prefix_placeholder", "t123")
461
+ starting_id = example.get("starting_id", "1")
462
+ docstring = example.get("docstring", "")
463
+ user_input = example.get("user_input", "")
464
+ metadata_lines = example.get("metadata", [])
465
+ reasoning_lines = example.get("reasoning", [])
466
+ response_text = example.get("response_text", "")
467
+
468
+ # Start building the XML structure, add the description and user input
469
+ xml_content = f"""<example>
470
+ <example_docstring>
471
+ {docstring}
472
+ </example_docstring>
473
+ <example_stimulus>
474
+ <{tag_prefix}stimulus starting_id="{starting_id}">
475
+ {user_input}
476
+ </{tag_prefix}stimulus>
477
+ <{tag_prefix}stimulus_metadata>
478
+ """
479
+
480
+ # Add metadata lines
481
+ for metadata_line in metadata_lines:
482
+ xml_content += f"{metadata_line}\n"
483
+
484
+ xml_content += f"""</{tag_prefix}stimulus_metadata>
485
+ </example_stimulus>
486
+ <example_response>
487
+ <{tag_prefix}reasoning>
488
+ """
489
+
490
+ # Add reasoning lines
491
+ for reasoning_line in reasoning_lines:
492
+ xml_content += f"{reasoning_line}\n"
493
+
494
+ xml_content += f"""</{tag_prefix}reasoning>
495
+ {response_text}"""
496
+
497
+ # Add action invocation section
498
+ if "action" in example:
499
+ action_data = example.get("action", {})
500
+ status_update = example.get("status_update", "")
501
+ agent_name = action_data.get("agent", "")
502
+ action_name = action_data.get("name", "")
503
+
504
+ xml_content += f"""
505
+ <{tag_prefix}status_update>{status_update}</{tag_prefix}status_update>
506
+ <{tag_prefix}invoke_action agent="{agent_name}" action="{action_name}">"""
507
+
508
+ # Handle parameters as dictionary
509
+ parameter_dict = action_data.get("parameters", {})
510
+ for param_name, param_value in parameter_dict.items():
511
+ xml_content += f"""
512
+ <{tag_prefix}parameter name="{param_name}">"""
513
+
514
+ # Handle parameter names and values (as lists)
515
+ if isinstance(param_value, list):
516
+ for line in param_value:
517
+ xml_content += f"\n{line}"
518
+ xml_content += "\n"
519
+ else:
520
+ # For simple string values
521
+ xml_content += f"{param_value}"
522
+
523
+ xml_content += f"</{tag_prefix}parameter>\n"
524
+
525
+ xml_content += f"</{tag_prefix}invoke_action>"
526
+
527
+ # Close the XML structure
528
+ xml_content += """
529
+ </example_response>
530
+ </example>
531
+ """
532
+
533
+ return xml_content
534
+
535
+ LONG_TERM_MEMORY_PROMPT = " - You are capable of remembering things and have long-term memory, this happens automatically."
@@ -392,6 +392,11 @@ class FileService(AutoExpiry, metaclass=AutoExpirySingletonMeta):
392
392
  url = url[5:-6].strip()
393
393
  if url.endswith('"') or url.endswith("'") or url.endswith(","):
394
394
  url = url[:-1]
395
+ if url.endswith(")"):
396
+ open_parenthesis_count = url.count("(")
397
+ close_parenthesis_count = url.count(")")
398
+ if close_parenthesis_count > open_parenthesis_count:
399
+ url = url[:-1]
395
400
  return url
396
401
 
397
402
  @staticmethod
@@ -40,7 +40,7 @@ BLOCK_TAG_KEYS = [
40
40
  Keys to be treated as tags in the file block, and not file attributes.
41
41
  """
42
42
 
43
- FS_URL_REGEX = r"""<url>\s*({protocol}:\/\/.+?)\s*<\/url>|{protocol}:\/\/[^\s\?]+(\s|$)|{protocol}:\/\/[^\n\?]+\?.*?(?=\n|$)|\"({protocol}:\/\/[^\"]+?)(\"|\n)|'({protocol}:\/\/[^']+?)('|\n)""".format(
43
+ FS_URL_REGEX = r"""<url>\s*({protocol}:\/\/.+?)\s*<\/url>|{protocol}:\/\/[^\s\?]+(\s|$)|{protocol}:\/\/[^\n\?]+\?.*?(?=\n|$|>)|\"({protocol}:\/\/[^\"]+?)(\"|\n)|'({protocol}:\/\/[^']+?)('|\n)""".format(
44
44
  protocol=FS_PROTOCOL
45
45
  )
46
46
  """
@@ -82,6 +82,8 @@ def apply_file_transformations(
82
82
  return file
83
83
  text_mime_type_regex = r"text/.*|.*csv|.*json|.*xml|.*yaml|.*x-yaml|.*txt"
84
84
  mime_type = metadata.get("mime_type", "")
85
+ if mime_type is None:
86
+ mime_type = ""
85
87
  name = metadata.get("name", "unknown")
86
88
  other = {
87
89
  "mime_type": mime_type,
@@ -126,6 +128,14 @@ def apply_file_transformations(
126
128
  return file
127
129
 
128
130
  if not isinstance(data, str):
129
- data = json.dumps(data)
131
+ # Convert bytes to string
132
+ if isinstance(data, bytes):
133
+ try:
134
+ data = data.decode("utf-8")
135
+ except UnicodeDecodeError:
136
+ data = base64.b64encode(data).decode("utf-8")
137
+ data = f"data:{mime_type};base64,{data}"
138
+ else:
139
+ data = json.dumps(data)
130
140
 
131
141
  return data
@@ -34,6 +34,8 @@ def get_str_type(value: str) -> str:
34
34
  Returns:
35
35
  - str: The type of the value.
36
36
  """
37
+ if not value:
38
+ return Types.NULL
37
39
  if value.isdigit():
38
40
  return Types.INT
39
41
  elif value.replace(".", "", 1).isdigit():
@@ -1,78 +1,54 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Union
3
2
 
4
3
  class BaseHistoryProvider(ABC):
5
4
 
6
5
  def __init__(self, config=None):
7
- self.config = config
8
- self.max_turns = self.config.get("max_turns")
9
- self.max_characters = self.config.get("max_characters")
10
- self.enforce_alternate_message_roles = self.config.get("enforce_alternate_message_roles")
11
-
12
- @abstractmethod
13
- def store_history(self, session_id: str, role: str, content: Union[str, dict]):
14
- """
15
- Store a new entry in the history.
16
-
17
- :param session_id: The session identifier.
18
- :param role: The role of the entry to be stored in the history.
19
- :param content: The content of the entry to be stored in the history.
20
- """
21
- raise NotImplementedError("Method not implemented")
22
-
6
+ self.config = config or {}
7
+
23
8
  @abstractmethod
24
- def get_history(self, session_id: str):
9
+ def get_all_sessions(self) -> list[str]:
25
10
  """
26
- Retrieve the entire history.
27
-
28
- :param session_id: The session identifier.
29
- :return: The complete history.
11
+ Retrieve all session identifiers.
30
12
  """
31
13
  raise NotImplementedError("Method not implemented")
32
14
 
33
15
  @abstractmethod
34
- def store_file(self, session_id: str, file: dict):
16
+ def get_session(self, session_id: str)->dict:
35
17
  """
36
- Store a file in the history.
18
+ Retrieve the session .
37
19
 
38
20
  :param session_id: The session identifier.
39
- :param file: The file metadata to be stored in the history.
21
+ :return: The session metadata.
40
22
  """
41
23
  raise NotImplementedError("Method not implemented")
42
-
24
+
43
25
  @abstractmethod
44
- def get_files(self, session_id: str):
26
+ def delete_session(self, session_id: str):
45
27
  """
46
- Retrieve the files for a session.
28
+ Delete the session.
47
29
 
48
30
  :param session_id: The session identifier.
49
- :return: The files for the session.
50
31
  """
51
32
  raise NotImplementedError("Method not implemented")
52
33
 
53
34
  @abstractmethod
54
- def get_session_meta(self, session_id: str):
35
+ def store_session(self, session_id: str, data: dict):
55
36
  """
56
- Retrieve the session metadata.
37
+ Store the session metadata.
57
38
 
58
39
  :param session_id: The session identifier.
59
- :return: The session metadata.
40
+ :param data: The session data to be stored.
60
41
  """
61
42
  raise NotImplementedError("Method not implemented")
43
+
62
44
 
63
- @abstractmethod
64
- def clear_history(self, session_id: str, keep_levels=0):
65
- """
66
- Clear the history and files, optionally keeping a specified number of recent entries.
67
-
68
- :param session_id: The session identifier.
69
- :param keep_levels: Number of most recent history entries to keep. Default is 0 (clear all).
45
+ def update_session(self, session_id: str, data: dict):
70
46
  """
71
- raise NotImplementedError("Method not implemented")
47
+ Update data in the store using the partial data provided.
72
48
 
73
- @abstractmethod
74
- def get_all_sessions(self) -> list[str]:
49
+ :param key: The key to update the data under.
50
+ :param data: The data to update.
75
51
  """
76
- Retrieve all session identifiers.
77
- """
78
- raise NotImplementedError("Method not implemented")
52
+ history = self.get_session(session_id).copy()
53
+ history.update(data)
54
+ self.store_session(session_id, history)
@@ -0,0 +1,74 @@
1
+ import json
2
+ import os
3
+ from .base_history_provider import BaseHistoryProvider
4
+
5
+ class FileHistoryProvider(BaseHistoryProvider):
6
+ """
7
+ A simple file-based history provider for storing session data.
8
+ """
9
+ def __init__(self, config=None):
10
+ super().__init__(config)
11
+
12
+ if not self.config.get("path"):
13
+ raise ValueError("Missing required configuration for FileHistoryProvider, Missing 'path' in configs.")
14
+
15
+ self.path = self.config.get("path")
16
+
17
+ if not self._exists(self.path):
18
+ os.makedirs(self.path, exist_ok=True)
19
+
20
+ def _get_key(self, session_id):
21
+ """
22
+ Generate a file path for a session.
23
+
24
+ :param session_id: The session identifier.
25
+ :return: A formatted file path.
26
+ """
27
+ return os.path.join(self.path, f"sessions_{session_id}_history.json")
28
+
29
+ def store_session(self, session_id: str, data: dict):
30
+ """
31
+ Store the session metadata.
32
+
33
+ :param session_id: The session identifier.
34
+ :param data: The session data to be stored.
35
+ """
36
+ file_path = self._get_key(session_id)
37
+ with open(file_path, "w", encoding="utf-8") as f:
38
+ f.write(json.dumps(data))
39
+
40
+ def get_session(self, session_id: str)->dict:
41
+ """
42
+ Retrieve the session.
43
+
44
+ :param session_id: The session identifier.
45
+ :return: The session metadata as a dictionary.
46
+ """
47
+ file_path = self._get_key(session_id)
48
+ if not self._exists(file_path):
49
+ return {}
50
+
51
+ try:
52
+ with open(file_path, "r", encoding="utf-8") as f:
53
+ return json.load(f)
54
+ except json.JSONDecodeError:
55
+ return {}
56
+
57
+ def get_all_sessions(self) -> list[str]:
58
+ """
59
+ Retrieve all session identifiers.
60
+ """
61
+ return [f[9:-13] for f in os.listdir(self.path) if f.startswith("sessions_") and f.endswith("_history.json")]
62
+
63
+ def delete_session(self, session_id: str):
64
+ """
65
+ Delete the session.
66
+
67
+ :param session_id: The session identifier.
68
+ """
69
+ file_path = self._get_key(session_id)
70
+ if self._exists(file_path):
71
+ os.remove(file_path)
72
+
73
+ def _exists(self, path: str):
74
+ return os.path.exists(path)
@@ -0,0 +1,40 @@
1
+
2
+
3
+ class HistoryProviderFactory:
4
+ """
5
+ Factory class for creating history provider instances.
6
+ """
7
+ HISTORY_PROVIDERS = ["redis", "memory", "file", "mongodb", "sql"]
8
+
9
+ @staticmethod
10
+ def has_provider(class_name):
11
+ """
12
+ Check if the history provider is supported.
13
+ """
14
+ return class_name in HistoryProviderFactory.HISTORY_PROVIDERS
15
+
16
+ @staticmethod
17
+ def get_provider_class(class_name):
18
+ """
19
+ Get the history provider class based on the class name.
20
+ """
21
+ if class_name not in HistoryProviderFactory.HISTORY_PROVIDERS:
22
+ raise ValueError(f"Unsupported history provider: {class_name}")
23
+ if class_name == "redis":
24
+ from .redis_history_provider import RedisHistoryProvider
25
+ return RedisHistoryProvider
26
+ elif class_name == "memory":
27
+ from .memory_history_provider import MemoryHistoryProvider
28
+ return MemoryHistoryProvider
29
+ elif class_name == "file":
30
+ from .file_history_provider import FileHistoryProvider
31
+ return FileHistoryProvider
32
+ elif class_name == "mongodb":
33
+ from .mongodb_history_provider import MongoDBHistoryProvider
34
+ return MongoDBHistoryProvider
35
+ elif class_name == "sql":
36
+ from .sql_history_provider import SQLHistoryProvider
37
+ return SQLHistoryProvider
38
+ else:
39
+ raise ValueError(f"Unsupported history provider: {class_name}")
40
+