griptape-nodes 0.62.2__py3-none-any.whl → 0.63.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.
Files changed (74) hide show
  1. griptape_nodes/cli/commands/libraries.py +6 -21
  2. griptape_nodes/drivers/thread_storage/__init__.py +15 -0
  3. griptape_nodes/drivers/thread_storage/base_thread_storage_driver.py +106 -0
  4. griptape_nodes/drivers/thread_storage/griptape_cloud_thread_storage_driver.py +213 -0
  5. griptape_nodes/drivers/thread_storage/local_thread_storage_driver.py +137 -0
  6. griptape_nodes/drivers/thread_storage/thread_storage_backend.py +10 -0
  7. griptape_nodes/node_library/library_registry.py +16 -9
  8. griptape_nodes/node_library/workflow_registry.py +1 -1
  9. griptape_nodes/retained_mode/events/agent_events.py +232 -9
  10. griptape_nodes/retained_mode/events/app_events.py +38 -0
  11. griptape_nodes/retained_mode/events/library_events.py +32 -3
  12. griptape_nodes/retained_mode/events/os_events.py +101 -1
  13. griptape_nodes/retained_mode/managers/agent_manager.py +335 -135
  14. griptape_nodes/retained_mode/managers/fitness_problems/__init__.py +1 -0
  15. griptape_nodes/retained_mode/managers/fitness_problems/libraries/__init__.py +59 -0
  16. griptape_nodes/retained_mode/managers/fitness_problems/libraries/advanced_library_load_failure_problem.py +33 -0
  17. griptape_nodes/retained_mode/managers/fitness_problems/libraries/after_library_callback_problem.py +32 -0
  18. griptape_nodes/retained_mode/managers/fitness_problems/libraries/before_library_callback_problem.py +32 -0
  19. griptape_nodes/retained_mode/managers/fitness_problems/libraries/create_config_category_problem.py +32 -0
  20. griptape_nodes/retained_mode/managers/fitness_problems/libraries/dependency_installation_failed_problem.py +32 -0
  21. griptape_nodes/retained_mode/managers/fitness_problems/libraries/deprecated_node_warning_problem.py +83 -0
  22. griptape_nodes/retained_mode/managers/fitness_problems/libraries/duplicate_library_problem.py +28 -0
  23. griptape_nodes/retained_mode/managers/fitness_problems/libraries/duplicate_node_registration_problem.py +44 -0
  24. griptape_nodes/retained_mode/managers/fitness_problems/libraries/engine_version_error_problem.py +28 -0
  25. griptape_nodes/retained_mode/managers/fitness_problems/libraries/insufficient_disk_space_problem.py +33 -0
  26. griptape_nodes/retained_mode/managers/fitness_problems/libraries/invalid_version_string_problem.py +32 -0
  27. griptape_nodes/retained_mode/managers/fitness_problems/libraries/library_json_decode_problem.py +28 -0
  28. griptape_nodes/retained_mode/managers/fitness_problems/libraries/library_load_exception_problem.py +32 -0
  29. griptape_nodes/retained_mode/managers/fitness_problems/libraries/library_not_found_problem.py +30 -0
  30. griptape_nodes/retained_mode/managers/fitness_problems/libraries/library_problem.py +20 -0
  31. griptape_nodes/retained_mode/managers/fitness_problems/libraries/library_schema_exception_problem.py +32 -0
  32. griptape_nodes/retained_mode/managers/fitness_problems/libraries/library_schema_validation_problem.py +38 -0
  33. griptape_nodes/retained_mode/managers/fitness_problems/libraries/modified_parameters_set_deprecation_warning_problem.py +44 -0
  34. griptape_nodes/retained_mode/managers/fitness_problems/libraries/modified_parameters_set_removed_problem.py +44 -0
  35. griptape_nodes/retained_mode/managers/fitness_problems/libraries/node_class_not_base_node_problem.py +40 -0
  36. griptape_nodes/retained_mode/managers/fitness_problems/libraries/node_class_not_found_problem.py +38 -0
  37. griptape_nodes/retained_mode/managers/fitness_problems/libraries/node_module_import_problem.py +53 -0
  38. griptape_nodes/retained_mode/managers/fitness_problems/libraries/sandbox_directory_missing_problem.py +28 -0
  39. griptape_nodes/retained_mode/managers/fitness_problems/libraries/ui_options_field_modified_incompatible_problem.py +44 -0
  40. griptape_nodes/retained_mode/managers/fitness_problems/libraries/ui_options_field_modified_warning_problem.py +35 -0
  41. griptape_nodes/retained_mode/managers/fitness_problems/libraries/update_config_category_problem.py +32 -0
  42. griptape_nodes/retained_mode/managers/fitness_problems/libraries/venv_creation_failed_problem.py +32 -0
  43. griptape_nodes/retained_mode/managers/fitness_problems/workflows/__init__.py +75 -0
  44. griptape_nodes/retained_mode/managers/fitness_problems/workflows/deprecated_node_in_workflow_problem.py +83 -0
  45. griptape_nodes/retained_mode/managers/fitness_problems/workflows/invalid_dependency_version_string_problem.py +38 -0
  46. griptape_nodes/retained_mode/managers/fitness_problems/workflows/invalid_library_version_string_problem.py +38 -0
  47. griptape_nodes/retained_mode/managers/fitness_problems/workflows/invalid_metadata_schema_problem.py +31 -0
  48. griptape_nodes/retained_mode/managers/fitness_problems/workflows/invalid_metadata_section_count_problem.py +31 -0
  49. griptape_nodes/retained_mode/managers/fitness_problems/workflows/invalid_toml_format_problem.py +30 -0
  50. griptape_nodes/retained_mode/managers/fitness_problems/workflows/library_not_registered_problem.py +35 -0
  51. griptape_nodes/retained_mode/managers/fitness_problems/workflows/library_version_below_required_problem.py +41 -0
  52. griptape_nodes/retained_mode/managers/fitness_problems/workflows/library_version_large_difference_problem.py +41 -0
  53. griptape_nodes/retained_mode/managers/fitness_problems/workflows/library_version_major_mismatch_problem.py +41 -0
  54. griptape_nodes/retained_mode/managers/fitness_problems/workflows/library_version_minor_difference_problem.py +41 -0
  55. griptape_nodes/retained_mode/managers/fitness_problems/workflows/missing_creation_date_problem.py +30 -0
  56. griptape_nodes/retained_mode/managers/fitness_problems/workflows/missing_last_modified_date_problem.py +30 -0
  57. griptape_nodes/retained_mode/managers/fitness_problems/workflows/missing_toml_section_problem.py +30 -0
  58. griptape_nodes/retained_mode/managers/fitness_problems/workflows/node_type_not_found_problem.py +51 -0
  59. griptape_nodes/retained_mode/managers/fitness_problems/workflows/workflow_not_found_problem.py +27 -0
  60. griptape_nodes/retained_mode/managers/fitness_problems/workflows/workflow_problem.py +20 -0
  61. griptape_nodes/retained_mode/managers/fitness_problems/workflows/workflow_schema_version_problem.py +39 -0
  62. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/local_file.py +17 -3
  63. griptape_nodes/retained_mode/managers/library_manager.py +226 -77
  64. griptape_nodes/retained_mode/managers/os_manager.py +172 -1
  65. griptape_nodes/retained_mode/managers/settings.py +5 -0
  66. griptape_nodes/retained_mode/managers/version_compatibility_manager.py +76 -51
  67. griptape_nodes/retained_mode/managers/workflow_manager.py +237 -159
  68. griptape_nodes/servers/static.py +18 -19
  69. griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_removal.py +16 -12
  70. griptape_nodes/version_compatibility/workflow_versions/v0_7_0/local_executor_argument_addition.py +6 -3
  71. {griptape_nodes-0.62.2.dist-info → griptape_nodes-0.63.0.dist-info}/METADATA +2 -1
  72. {griptape_nodes-0.62.2.dist-info → griptape_nodes-0.63.0.dist-info}/RECORD +74 -21
  73. {griptape_nodes-0.62.2.dist-info → griptape_nodes-0.63.0.dist-info}/WHEEL +0 -0
  74. {griptape_nodes-0.62.2.dist-info → griptape_nodes-0.63.0.dist-info}/entry_points.txt +0 -0
@@ -2,7 +2,6 @@
2
2
 
3
3
  import asyncio
4
4
  import shutil
5
- import stat
6
5
  import tarfile
7
6
  import tempfile
8
7
  from pathlib import Path
@@ -17,6 +16,7 @@ from griptape_nodes.cli.shared import (
17
16
  NODES_TARBALL_URL,
18
17
  console,
19
18
  )
19
+ from griptape_nodes.retained_mode.events.os_events import DeleteFileRequest, DeleteFileResultSuccess
20
20
  from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
21
21
  from griptape_nodes.utils.version_utils import get_current_version, get_install_source
22
22
 
@@ -81,7 +81,11 @@ async def _sync_libraries(*, load_libraries_from_config: bool = True) -> None:
81
81
  if library_dir.is_dir():
82
82
  dest_library_dir = dest_nodes / library_dir.name
83
83
  if dest_library_dir.exists():
84
- shutil.rmtree(dest_library_dir, onexc=_remove_readonly)
84
+ # Use DeleteFileRequest for centralized deletion with Windows compatibility
85
+ request = DeleteFileRequest(path=str(dest_library_dir), workspace_only=False)
86
+ result = await GriptapeNodes.OSManager().on_delete_file_request(request)
87
+ if not isinstance(result, DeleteFileResultSuccess):
88
+ console.print(f"[yellow]Warning: Failed to delete existing library {library_dir.name}[/yellow]")
85
89
  shutil.copytree(library_dir, dest_library_dir)
86
90
  console.print(f"[green]Synced library: {library_dir.name}[/green]")
87
91
 
@@ -95,22 +99,3 @@ async def _sync_libraries(*, load_libraries_from_config: bool = True) -> None:
95
99
  console.print(f"[red]Error initializing libraries: {e}[/red]")
96
100
 
97
101
  console.print("[bold green]Libraries synced.[/bold green]")
98
-
99
-
100
- def _remove_readonly(func, path, excinfo) -> None: # noqa: ANN001, ARG001
101
- """Handles read-only files and long paths on Windows during shutil.rmtree.
102
-
103
- https://stackoverflow.com/a/50924863
104
- """
105
- if not GriptapeNodes.OSManager().is_windows():
106
- return
107
-
108
- long_path = Path(GriptapeNodes.OSManager().normalize_path_for_platform(Path(path)))
109
-
110
- try:
111
- Path.chmod(long_path, stat.S_IWRITE)
112
- func(long_path)
113
- except Exception as e:
114
- console.print(f"[red]Error removing read-only file: {path}[/red]")
115
- console.print(f"[red]Details: {e}[/red]")
116
- raise
@@ -0,0 +1,15 @@
1
+ """Thread storage drivers."""
2
+
3
+ from griptape_nodes.drivers.thread_storage.base_thread_storage_driver import BaseThreadStorageDriver
4
+ from griptape_nodes.drivers.thread_storage.griptape_cloud_thread_storage_driver import (
5
+ GriptapeCloudThreadStorageDriver,
6
+ )
7
+ from griptape_nodes.drivers.thread_storage.local_thread_storage_driver import LocalThreadStorageDriver
8
+ from griptape_nodes.drivers.thread_storage.thread_storage_backend import ThreadStorageBackend
9
+
10
+ __all__ = [
11
+ "BaseThreadStorageDriver",
12
+ "GriptapeCloudThreadStorageDriver",
13
+ "LocalThreadStorageDriver",
14
+ "ThreadStorageBackend",
15
+ ]
@@ -0,0 +1,106 @@
1
+ """Base thread storage driver abstract class."""
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+ from griptape.drivers.memory.conversation import BaseConversationMemoryDriver
6
+
7
+ from griptape_nodes.retained_mode.events.agent_events import ThreadMetadata
8
+ from griptape_nodes.retained_mode.managers.config_manager import ConfigManager
9
+ from griptape_nodes.retained_mode.managers.secrets_manager import SecretsManager
10
+
11
+
12
+ class BaseThreadStorageDriver(ABC):
13
+ """Abstract base class for thread storage backends."""
14
+
15
+ def __init__(self, config_manager: ConfigManager, secrets_manager: SecretsManager) -> None:
16
+ """Initialize the thread storage driver.
17
+
18
+ Args:
19
+ config_manager: Configuration manager instance
20
+ secrets_manager: Secrets manager instance
21
+ """
22
+ self.config_manager = config_manager
23
+ self.secrets_manager = secrets_manager
24
+
25
+ @abstractmethod
26
+ def create_thread(self, title: str | None = None, local_id: str | None = None) -> tuple[str, dict]:
27
+ """Create a new thread with metadata.
28
+
29
+ Args:
30
+ title: Optional thread title
31
+ local_id: Optional client-side identifier
32
+
33
+ Returns:
34
+ Tuple of (thread_id, metadata_dict)
35
+ """
36
+ ...
37
+
38
+ @abstractmethod
39
+ def get_thread_metadata(self, thread_id: str) -> dict:
40
+ """Get metadata for a thread.
41
+
42
+ Args:
43
+ thread_id: The thread identifier
44
+
45
+ Returns:
46
+ Metadata dictionary
47
+ """
48
+ ...
49
+
50
+ @abstractmethod
51
+ def update_thread_metadata(self, thread_id: str, **updates) -> dict:
52
+ """Update thread metadata.
53
+
54
+ Args:
55
+ thread_id: The thread identifier
56
+ **updates: Key-value pairs to update in metadata
57
+
58
+ Returns:
59
+ Updated metadata dictionary
60
+ """
61
+ ...
62
+
63
+ @abstractmethod
64
+ def list_threads(self) -> list[ThreadMetadata]:
65
+ """List all threads with metadata.
66
+
67
+ Returns:
68
+ List of ThreadMetadata objects
69
+ """
70
+ ...
71
+
72
+ @abstractmethod
73
+ def delete_thread(self, thread_id: str) -> None:
74
+ """Delete a thread.
75
+
76
+ Args:
77
+ thread_id: The thread identifier
78
+
79
+ Raises:
80
+ ValueError: If thread is not archived or doesn't exist
81
+ """
82
+ ...
83
+
84
+ @abstractmethod
85
+ def thread_exists(self, thread_id: str) -> bool:
86
+ """Check if a thread exists.
87
+
88
+ Args:
89
+ thread_id: The thread identifier
90
+
91
+ Returns:
92
+ True if thread exists, False otherwise
93
+ """
94
+ ...
95
+
96
+ @abstractmethod
97
+ def get_conversation_memory_driver(self, thread_id: str | None) -> BaseConversationMemoryDriver:
98
+ """Get the appropriate conversation memory driver for this thread.
99
+
100
+ Args:
101
+ thread_id: The thread identifier (can be None for GTC backend)
102
+
103
+ Returns:
104
+ Conversation memory driver instance
105
+ """
106
+ ...
@@ -0,0 +1,213 @@
1
+ """Griptape Cloud thread storage driver."""
2
+
3
+ import logging
4
+ import os
5
+ from datetime import UTC, datetime
6
+ from urllib.parse import urljoin
7
+
8
+ import httpx
9
+ from griptape.drivers.memory.conversation import BaseConversationMemoryDriver
10
+ from griptape.drivers.memory.conversation.griptape_cloud import GriptapeCloudConversationMemoryDriver
11
+
12
+ from griptape_nodes.drivers.thread_storage.base_thread_storage_driver import BaseThreadStorageDriver
13
+ from griptape_nodes.retained_mode.events.agent_events import ThreadMetadata
14
+
15
+ API_KEY_ENV_VAR = "GT_CLOUD_API_KEY"
16
+ HTTP_NOT_FOUND = 404
17
+ logger = logging.getLogger("griptape_nodes")
18
+
19
+
20
+ class GriptapeCloudThreadStorageDriver(BaseThreadStorageDriver):
21
+ """Griptape Cloud implementation of thread storage."""
22
+
23
+ @property
24
+ def base_url(self) -> str:
25
+ """Get the base URL for Griptape Cloud API."""
26
+ return os.environ.get("GT_CLOUD_BASE_URL", "https://cloud.griptape.ai")
27
+
28
+ @property
29
+ def _headers(self) -> dict:
30
+ """Get the authorization headers for API requests."""
31
+ api_key = self.secrets_manager.get_secret(API_KEY_ENV_VAR)
32
+ if not api_key:
33
+ msg = f"Secret '{API_KEY_ENV_VAR}' not found for Griptape Cloud thread storage"
34
+ raise ValueError(msg)
35
+ return {"Authorization": f"Bearer {api_key}"}
36
+
37
+ def _get_url(self, path: str) -> str:
38
+ """Construct full API URL from path."""
39
+ return urljoin(self.base_url, path)
40
+
41
+ def create_thread(self, title: str | None = None, local_id: str | None = None) -> tuple[str, dict]:
42
+ url = self._get_url("/api/threads")
43
+
44
+ now = datetime.now(UTC).isoformat()
45
+
46
+ # Build metadata for the request
47
+ metadata = {
48
+ "created_at": now,
49
+ "updated_at": now,
50
+ }
51
+ if local_id is not None:
52
+ metadata["local_id"] = local_id
53
+
54
+ # Build request body
55
+ request_body: dict[str, str | dict] = {"metadata": metadata}
56
+ if title is not None:
57
+ request_body["name"] = title
58
+
59
+ try:
60
+ response = httpx.post(url, json=request_body, headers=self._headers)
61
+ response.raise_for_status()
62
+ except httpx.HTTPStatusError as e:
63
+ msg = f"Failed to create thread in Griptape Cloud: {e}"
64
+ logger.error(msg)
65
+ raise RuntimeError(msg) from e
66
+
67
+ response_data = response.json()
68
+ thread_id = response_data["thread_id"]
69
+
70
+ # Build metadata dict to return (matching local driver behavior)
71
+ result_metadata = response_data.get("metadata", {})
72
+ if title is not None:
73
+ result_metadata["title"] = title
74
+
75
+ return thread_id, result_metadata
76
+
77
+ def get_thread_metadata(self, thread_id: str) -> dict:
78
+ url = self._get_url(f"/api/threads/{thread_id}")
79
+
80
+ try:
81
+ response = httpx.get(url, headers=self._headers)
82
+ response.raise_for_status()
83
+ except httpx.HTTPStatusError as e:
84
+ if e.response.status_code == HTTP_NOT_FOUND:
85
+ return {}
86
+ msg = f"Failed to get thread {thread_id} from Griptape Cloud: {e}"
87
+ logger.error(msg)
88
+ raise RuntimeError(msg) from e
89
+
90
+ response_data = response.json()
91
+
92
+ # Extract metadata from response
93
+ metadata = response_data.get("metadata", {})
94
+
95
+ # Map GTC's "name" field to our "title" field for consistency
96
+ if response_data.get("name"):
97
+ metadata["title"] = response_data["name"]
98
+
99
+ return metadata
100
+
101
+ def update_thread_metadata(self, thread_id: str, **updates) -> dict:
102
+ # First, get current metadata
103
+ current_metadata = self.get_thread_metadata(thread_id)
104
+
105
+ now = datetime.now(UTC).isoformat()
106
+
107
+ # Merge updates into current metadata
108
+ for key, value in updates.items():
109
+ if value is not None:
110
+ current_metadata[key] = value
111
+
112
+ current_metadata["updated_at"] = now
113
+
114
+ if "created_at" not in current_metadata:
115
+ current_metadata["created_at"] = now
116
+
117
+ url = self._get_url(f"/api/threads/{thread_id}")
118
+
119
+ # Build request body
120
+ request_body: dict[str, str | dict] = {"metadata": current_metadata}
121
+
122
+ # If title was updated, also set the name field
123
+ if "title" in updates and updates["title"] is not None:
124
+ request_body["name"] = updates["title"]
125
+
126
+ try:
127
+ response = httpx.patch(url, json=request_body, headers=self._headers)
128
+ response.raise_for_status()
129
+ except httpx.HTTPStatusError as e:
130
+ msg = f"Failed to update thread {thread_id} in Griptape Cloud: {e}"
131
+ logger.error(msg)
132
+ raise RuntimeError(msg) from e
133
+
134
+ response_data = response.json()
135
+
136
+ # Return updated metadata
137
+ result_metadata = response_data.get("metadata", {})
138
+ if response_data.get("name"):
139
+ result_metadata["title"] = response_data["name"]
140
+
141
+ return result_metadata
142
+
143
+ def list_threads(self) -> list[ThreadMetadata]:
144
+ url = self._get_url("/api/threads")
145
+
146
+ try:
147
+ response = httpx.get(url, headers=self._headers)
148
+ response.raise_for_status()
149
+ except httpx.HTTPStatusError as e:
150
+ msg = f"Failed to list threads from Griptape Cloud: {e}"
151
+ logger.error(msg)
152
+ raise RuntimeError(msg) from e
153
+
154
+ response_data = response.json()
155
+ threads_data = response_data.get("threads", [])
156
+
157
+ threads = []
158
+ for thread_data in threads_data:
159
+ thread_id = thread_data.get("thread_id")
160
+ if not thread_id:
161
+ continue
162
+
163
+ metadata = thread_data.get("metadata", {})
164
+
165
+ thread_metadata = ThreadMetadata(
166
+ thread_id=thread_id,
167
+ title=thread_data.get("name"),
168
+ created_at=metadata.get("created_at"),
169
+ updated_at=metadata.get("updated_at"),
170
+ message_count=thread_data.get("message_count", 0),
171
+ archived=metadata.get("archived", False),
172
+ local_id=metadata.get("local_id"),
173
+ )
174
+ threads.append(thread_metadata)
175
+
176
+ # Sort by updated_at descending (most recent first)
177
+ threads.sort(key=lambda t: t.updated_at or "", reverse=True)
178
+
179
+ return threads
180
+
181
+ def delete_thread(self, thread_id: str) -> None:
182
+ if not self.thread_exists(thread_id):
183
+ msg = f"Thread {thread_id} not found"
184
+ raise ValueError(msg)
185
+
186
+ meta = self.get_thread_metadata(thread_id)
187
+ if not meta.get("archived", False):
188
+ msg = f"Cannot delete thread {thread_id}. Archive it first."
189
+ raise ValueError(msg)
190
+
191
+ url = self._get_url(f"/api/threads/{thread_id}")
192
+
193
+ try:
194
+ response = httpx.delete(url, headers=self._headers)
195
+ response.raise_for_status()
196
+ except httpx.HTTPStatusError as e:
197
+ msg = f"Failed to delete thread {thread_id} from Griptape Cloud: {e}"
198
+ logger.error(msg)
199
+ raise RuntimeError(msg) from e
200
+
201
+ def thread_exists(self, thread_id: str) -> bool:
202
+ try:
203
+ meta = self.get_thread_metadata(thread_id)
204
+ return bool(meta)
205
+ except Exception:
206
+ return False
207
+
208
+ def get_conversation_memory_driver(self, thread_id: str | None) -> BaseConversationMemoryDriver:
209
+ api_key = self.secrets_manager.get_secret(API_KEY_ENV_VAR)
210
+ if not api_key:
211
+ msg = f"Secret '{API_KEY_ENV_VAR}' not found for Griptape Cloud thread storage"
212
+ raise ValueError(msg)
213
+ return GriptapeCloudConversationMemoryDriver(api_key=api_key, thread_id=thread_id)
@@ -0,0 +1,137 @@
1
+ """Local filesystem thread storage driver."""
2
+
3
+ import uuid
4
+ from datetime import UTC, datetime
5
+ from pathlib import Path
6
+
7
+ from griptape.drivers.memory.conversation import BaseConversationMemoryDriver
8
+ from griptape.drivers.memory.conversation.local import LocalConversationMemoryDriver
9
+ from griptape.memory.structure import ConversationMemory
10
+
11
+ from griptape_nodes.drivers.thread_storage.base_thread_storage_driver import BaseThreadStorageDriver
12
+ from griptape_nodes.retained_mode.events.agent_events import ThreadMetadata
13
+ from griptape_nodes.retained_mode.managers.config_manager import ConfigManager
14
+ from griptape_nodes.retained_mode.managers.secrets_manager import SecretsManager
15
+
16
+
17
+ class LocalThreadStorageDriver(BaseThreadStorageDriver):
18
+ """Local filesystem implementation of thread storage."""
19
+
20
+ def __init__(self, threads_directory: Path, config_manager: ConfigManager, secrets_manager: SecretsManager) -> None:
21
+ """Initialize the local thread storage driver.
22
+
23
+ Args:
24
+ threads_directory: Directory for storing thread data
25
+ config_manager: Configuration manager instance
26
+ secrets_manager: Secrets manager instance
27
+ """
28
+ super().__init__(config_manager, secrets_manager)
29
+ self.threads_directory = threads_directory
30
+
31
+ def create_thread(self, title: str | None = None, local_id: str | None = None) -> tuple[str, dict]:
32
+ thread_id = str(uuid.uuid4())
33
+
34
+ conversation_memory = self._get_or_create_conversation_memory(thread_id)
35
+
36
+ now = datetime.now(UTC).isoformat()
37
+ conversation_memory.meta = {
38
+ "created_at": now,
39
+ "updated_at": now,
40
+ }
41
+
42
+ if title is not None:
43
+ conversation_memory.meta["title"] = title
44
+ if local_id is not None:
45
+ conversation_memory.meta["local_id"] = local_id
46
+
47
+ conversation_memory.conversation_memory_driver.store(conversation_memory.runs, conversation_memory.meta)
48
+
49
+ return thread_id, conversation_memory.meta
50
+
51
+ def get_thread_metadata(self, thread_id: str) -> dict:
52
+ conversation_memory = self._get_or_create_conversation_memory(thread_id)
53
+ if conversation_memory.meta is None:
54
+ return {}
55
+ return conversation_memory.meta
56
+
57
+ def update_thread_metadata(self, thread_id: str, **updates) -> dict:
58
+ conversation_memory = self._get_or_create_conversation_memory(thread_id)
59
+ now = datetime.now(UTC).isoformat()
60
+
61
+ if conversation_memory.meta is None:
62
+ conversation_memory.meta = {}
63
+
64
+ for key, value in updates.items():
65
+ if value is not None:
66
+ conversation_memory.meta[key] = value
67
+
68
+ conversation_memory.meta["updated_at"] = now
69
+
70
+ if "created_at" not in conversation_memory.meta:
71
+ conversation_memory.meta["created_at"] = now
72
+
73
+ conversation_memory.conversation_memory_driver.store(conversation_memory.runs, conversation_memory.meta)
74
+
75
+ return conversation_memory.meta
76
+
77
+ def list_threads(self) -> list[ThreadMetadata]:
78
+ threads = []
79
+
80
+ if not self.threads_directory.exists():
81
+ return threads
82
+
83
+ thread_ids = [
84
+ thread_file.stem.replace("thread_", "") for thread_file in self.threads_directory.glob("thread_*.json")
85
+ ]
86
+
87
+ for thread_id in thread_ids:
88
+ meta = self.get_thread_metadata(thread_id)
89
+ conversation_memory = self._get_or_create_conversation_memory(thread_id)
90
+ message_count = len(conversation_memory.runs)
91
+
92
+ threads.append(
93
+ ThreadMetadata(
94
+ thread_id=thread_id,
95
+ title=meta.get("title"),
96
+ created_at=meta.get("created_at", ""),
97
+ updated_at=meta.get("updated_at", ""),
98
+ message_count=message_count,
99
+ archived=meta.get("archived", False),
100
+ local_id=meta.get("local_id"),
101
+ )
102
+ )
103
+
104
+ threads.sort(key=lambda t: t.updated_at, reverse=True)
105
+
106
+ return threads
107
+
108
+ def delete_thread(self, thread_id: str) -> None:
109
+ thread_file = self.threads_directory / f"thread_{thread_id}.json"
110
+
111
+ if not thread_file.exists():
112
+ msg = f"Thread {thread_id} not found"
113
+ raise ValueError(msg)
114
+
115
+ meta = self.get_thread_metadata(thread_id)
116
+ if not meta.get("archived", False):
117
+ msg = f"Cannot delete thread {thread_id}. Archive it first."
118
+ raise ValueError(msg)
119
+
120
+ thread_file.unlink()
121
+
122
+ def thread_exists(self, thread_id: str) -> bool:
123
+ thread_file = self.threads_directory / f"thread_{thread_id}.json"
124
+ return thread_file.exists()
125
+
126
+ def get_conversation_memory_driver(self, thread_id: str | None) -> BaseConversationMemoryDriver:
127
+ if thread_id is None:
128
+ msg = "thread_id is required for local storage backend"
129
+ raise ValueError(msg)
130
+
131
+ thread_file = self.threads_directory / f"thread_{thread_id}.json"
132
+ return LocalConversationMemoryDriver(persist_file=str(thread_file))
133
+
134
+ def _get_or_create_conversation_memory(self, thread_id: str) -> ConversationMemory:
135
+ """Get or create ConversationMemory instance for a thread."""
136
+ driver = self.get_conversation_memory_driver(thread_id)
137
+ return ConversationMemory(conversation_memory_driver=driver)
@@ -0,0 +1,10 @@
1
+ """Thread storage backend enumeration."""
2
+
3
+ from enum import StrEnum
4
+
5
+
6
+ class ThreadStorageBackend(StrEnum):
7
+ """Enumeration of available thread storage backends."""
8
+
9
+ LOCAL = "local"
10
+ GTC = "gtc"
@@ -5,11 +5,15 @@ from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple
5
5
 
6
6
  from pydantic import BaseModel, Field
7
7
 
8
+ from griptape_nodes.retained_mode.managers.fitness_problems.libraries.duplicate_node_registration_problem import (
9
+ DuplicateNodeRegistrationProblem,
10
+ )
8
11
  from griptape_nodes.utils.metaclasses import SingletonMeta
9
12
 
10
13
  if TYPE_CHECKING:
11
14
  from griptape_nodes.exe_types.node_types import BaseNode
12
15
  from griptape_nodes.node_library.advanced_node_library import AdvancedNodeLibrary
16
+ from griptape_nodes.retained_mode.managers.fitness_problems.libraries.library_problem import LibraryProblem
13
17
 
14
18
  logger = logging.getLogger("griptape_nodes")
15
19
 
@@ -177,16 +181,19 @@ class LibraryRegistry(metaclass=SingletonMeta):
177
181
  return sorted_list
178
182
 
179
183
  @classmethod
180
- def register_node_type_from_library(cls, library: Library, node_class_name: str) -> str | None:
181
- """Register a node type from a library. Returns an error string for forensics."""
184
+ def register_node_type_from_library(cls, library: Library, node_class_name: str) -> LibraryProblem | None:
185
+ """Register a node type from a library. Returns a LibraryProblem if registration fails."""
182
186
  # Does a node class of this name already exist?
183
187
  library_collisions = LibraryRegistry.get_libraries_with_node_type(node_class_name)
184
188
  if library_collisions:
185
189
  library_data = library.get_library_data()
186
190
  if library_data.name in library_collisions:
187
- details = f"Attempted to register Node class '{node_class_name}' from Library '{library_data.name}', but a Node with that name from that Library was already registered. Check to ensure you aren't re-adding the same libraries multiple times."
188
- logger.error(details)
189
- return details
191
+ logger.error(
192
+ "Attempted to register node class '%s' from library '%s', but a node with that name from that library was already registered",
193
+ node_class_name,
194
+ library_data.name,
195
+ )
196
+ return DuplicateNodeRegistrationProblem(class_name=node_class_name, library_name=library_data.name)
190
197
 
191
198
  return None
192
199
 
@@ -301,19 +308,19 @@ class Library:
301
308
  self._node_metadata = {}
302
309
  self._advanced_library = advanced_library
303
310
 
304
- def register_new_node_type(self, node_class: type[BaseNode], metadata: NodeMetadata) -> str | None:
305
- """Register a new node type in this library. Returns an error string for forensics, or None if all clear."""
311
+ def register_new_node_type(self, node_class: type[BaseNode], metadata: NodeMetadata) -> LibraryProblem | None:
312
+ """Register a new node type in this library. Returns a LibraryProblem if registration fails, or None if all clear."""
306
313
  # We only need to register the name of the node within the library.
307
314
  node_class_as_str = node_class.__name__
308
315
 
309
316
  # Let the registry know.
310
- registry_details = LibraryRegistry.register_node_type_from_library(
317
+ library_problem = LibraryRegistry.register_node_type_from_library(
311
318
  library=self, node_class_name=node_class_as_str
312
319
  )
313
320
 
314
321
  self._node_types[node_class_as_str] = node_class
315
322
  self._node_metadata[node_class_as_str] = metadata
316
- return registry_details
323
+ return library_problem
317
324
 
318
325
  def get_library_data(self) -> LibrarySchema:
319
326
  return self._library_data
@@ -45,7 +45,7 @@ class WorkflowShape(BaseModel):
45
45
 
46
46
 
47
47
  class WorkflowMetadata(BaseModel):
48
- LATEST_SCHEMA_VERSION: ClassVar[str] = "0.12.0"
48
+ LATEST_SCHEMA_VERSION: ClassVar[str] = "0.13.0"
49
49
 
50
50
  name: str
51
51
  schema_version: str