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
@@ -4,6 +4,7 @@ import logging
4
4
  import os
5
5
  import threading
6
6
  import uuid
7
+ from collections.abc import Iterator
7
8
  from typing import TYPE_CHECKING, ClassVar
8
9
 
9
10
  from attrs import define, field
@@ -22,21 +23,41 @@ from griptape.utils.decorators import activity
22
23
  from json_repair import repair_json
23
24
  from pydantic import create_model
24
25
  from schema import Literal, Schema
26
+ from xdg_base_dirs import xdg_data_home
25
27
 
28
+ from griptape_nodes.drivers.thread_storage import (
29
+ GriptapeCloudThreadStorageDriver,
30
+ LocalThreadStorageDriver,
31
+ )
26
32
  from griptape_nodes.retained_mode.events.agent_events import (
27
33
  AgentStreamEvent,
34
+ ArchiveThreadRequest,
35
+ ArchiveThreadResultFailure,
36
+ ArchiveThreadResultSuccess,
28
37
  ConfigureAgentRequest,
29
38
  ConfigureAgentResultFailure,
30
39
  ConfigureAgentResultSuccess,
40
+ CreateThreadRequest,
41
+ CreateThreadResultFailure,
42
+ CreateThreadResultSuccess,
43
+ DeleteThreadRequest,
44
+ DeleteThreadResultFailure,
45
+ DeleteThreadResultSuccess,
31
46
  GetConversationMemoryRequest,
32
47
  GetConversationMemoryResultFailure,
33
48
  GetConversationMemoryResultSuccess,
34
- ResetAgentConversationMemoryRequest,
35
- ResetAgentConversationMemoryResultFailure,
36
- ResetAgentConversationMemoryResultSuccess,
49
+ ListThreadsRequest,
50
+ ListThreadsResultFailure,
51
+ ListThreadsResultSuccess,
52
+ RenameThreadRequest,
53
+ RenameThreadResultFailure,
54
+ RenameThreadResultSuccess,
37
55
  RunAgentRequest,
38
56
  RunAgentResultFailure,
39
57
  RunAgentResultSuccess,
58
+ UnarchiveThreadRequest,
59
+ UnarchiveThreadResultFailure,
60
+ UnarchiveThreadResultSuccess,
40
61
  )
41
62
  from griptape_nodes.retained_mode.events.app_events import AppInitializationComplete
42
63
  from griptape_nodes.retained_mode.events.base_events import ExecutionEvent, ExecutionGriptapeNodeEvent, ResultPayload
@@ -104,27 +125,321 @@ class AgentManager:
104
125
  }
105
126
 
106
127
  def __init__(self, static_files_manager: StaticFilesManager, event_manager: EventManager | None = None) -> None:
107
- self.conversation_memory = ConversationMemory()
108
128
  self.prompt_driver = None
109
129
  self.image_tool = None
110
130
  self.mcp_tool = None
111
131
  self.static_files_manager = static_files_manager
112
132
 
133
+ # Thread management
134
+ self._threads_dir = xdg_data_home() / "griptape_nodes" / "threads"
135
+ self._threads_dir.mkdir(parents=True, exist_ok=True)
136
+
137
+ # Initialize thread storage driver based on config
138
+ self.thread_storage_driver = self._initialize_thread_storage_driver()
139
+
113
140
  if event_manager is not None:
141
+ # Existing handlers
114
142
  event_manager.assign_manager_to_request_type(RunAgentRequest, self.on_handle_run_agent_request)
115
143
  event_manager.assign_manager_to_request_type(ConfigureAgentRequest, self.on_handle_configure_agent_request)
116
144
  event_manager.assign_manager_to_request_type(
117
- ResetAgentConversationMemoryRequest, self.on_handle_reset_agent_conversation_memory_request
145
+ GetConversationMemoryRequest, self.on_handle_get_conversation_memory_request
118
146
  )
147
+
148
+ # New thread management handlers
149
+ event_manager.assign_manager_to_request_type(CreateThreadRequest, self.on_handle_create_thread_request)
150
+ event_manager.assign_manager_to_request_type(ListThreadsRequest, self.on_handle_list_threads_request)
151
+ event_manager.assign_manager_to_request_type(DeleteThreadRequest, self.on_handle_delete_thread_request)
152
+ event_manager.assign_manager_to_request_type(RenameThreadRequest, self.on_handle_rename_thread_request)
153
+ event_manager.assign_manager_to_request_type(ArchiveThreadRequest, self.on_handle_archive_thread_request)
119
154
  event_manager.assign_manager_to_request_type(
120
- GetConversationMemoryRequest, self.on_handle_get_conversation_memory_request
155
+ UnarchiveThreadRequest, self.on_handle_unarchive_thread_request
121
156
  )
157
+
122
158
  event_manager.add_listener_to_app_event(
123
159
  AppInitializationComplete,
124
160
  self.on_app_initialization_complete,
125
161
  )
126
162
  # TODO: Listen for shutdown event (https://github.com/griptape-ai/griptape-nodes/issues/2149) to stop mcp server
127
163
 
164
+ async def on_handle_run_agent_request(self, request: RunAgentRequest) -> ResultPayload:
165
+ if self.prompt_driver is None:
166
+ self.prompt_driver = self._initialize_prompt_driver()
167
+ if self.image_tool is None:
168
+ self.image_tool = self._initialize_image_tool()
169
+ if self.mcp_tool is None:
170
+ self.mcp_tool = self._initialize_mcp_tool()
171
+ try:
172
+ return await asyncio.to_thread(self._on_handle_run_agent_request, request)
173
+ except Exception as e:
174
+ err_msg = f"Error handling run agent request: {e}"
175
+ return RunAgentResultFailure(error=ErrorArtifact(e).to_dict(), result_details=err_msg)
176
+
177
+ def on_handle_configure_agent_request(self, request: ConfigureAgentRequest) -> ResultPayload:
178
+ try:
179
+ if self.prompt_driver is None:
180
+ self.prompt_driver = self._initialize_prompt_driver()
181
+ for key, value in request.prompt_driver.items():
182
+ setattr(self.prompt_driver, key, value)
183
+ except Exception as e:
184
+ details = f"Error configuring agent: {e}"
185
+ logger.error(details)
186
+ return ConfigureAgentResultFailure(result_details=details)
187
+ return ConfigureAgentResultSuccess(result_details="Agent configured successfully.")
188
+
189
+ def on_handle_get_conversation_memory_request(self, request: GetConversationMemoryRequest) -> ResultPayload:
190
+ try:
191
+ thread_id = request.thread_id
192
+
193
+ driver = self.thread_storage_driver.get_conversation_memory_driver(thread_id)
194
+ conversation_memory = ConversationMemory(conversation_memory_driver=driver)
195
+ runs = conversation_memory.runs
196
+
197
+ except Exception as e:
198
+ details = f"Error getting conversation memory: {e}"
199
+ logger.error(details)
200
+ return GetConversationMemoryResultFailure(result_details=details)
201
+
202
+ return GetConversationMemoryResultSuccess(
203
+ runs=runs, thread_id=thread_id, result_details="Conversation memory retrieved successfully."
204
+ )
205
+
206
+ def on_handle_create_thread_request(self, request: CreateThreadRequest) -> ResultPayload:
207
+ try:
208
+ thread_id, meta = self.thread_storage_driver.create_thread(title=request.title, local_id=request.local_id)
209
+
210
+ return CreateThreadResultSuccess(
211
+ thread_id=thread_id,
212
+ title=meta.get("title"),
213
+ created_at=meta["created_at"],
214
+ updated_at=meta["updated_at"],
215
+ result_details="Thread created successfully.",
216
+ )
217
+ except Exception as e:
218
+ details = f"Error creating thread: {e}"
219
+ logger.error(details)
220
+ return CreateThreadResultFailure(result_details=details)
221
+
222
+ def on_handle_list_threads_request(self, _: ListThreadsRequest) -> ResultPayload:
223
+ try:
224
+ threads = self.thread_storage_driver.list_threads()
225
+ return ListThreadsResultSuccess(threads=threads, result_details="Threads retrieved successfully.")
226
+ except Exception as e:
227
+ details = f"Error listing threads: {e}"
228
+ logger.error(details)
229
+ return ListThreadsResultFailure(result_details=details)
230
+
231
+ def on_handle_delete_thread_request(self, request: DeleteThreadRequest) -> ResultPayload:
232
+ try:
233
+ self.thread_storage_driver.delete_thread(request.thread_id)
234
+ return DeleteThreadResultSuccess(thread_id=request.thread_id, result_details="Thread deleted successfully.")
235
+ except ValueError as e:
236
+ details = str(e)
237
+ logger.error(details)
238
+ return DeleteThreadResultFailure(result_details=details)
239
+ except Exception as e:
240
+ details = f"Error deleting thread: {e}"
241
+ logger.error(details)
242
+ return DeleteThreadResultFailure(result_details=details)
243
+
244
+ def on_handle_rename_thread_request(self, request: RenameThreadRequest) -> ResultPayload:
245
+ try:
246
+ if not self.thread_storage_driver.thread_exists(request.thread_id):
247
+ details = f"Thread {request.thread_id} not found"
248
+ logger.error(details)
249
+ return RenameThreadResultFailure(result_details=details)
250
+
251
+ updated_meta = self.thread_storage_driver.update_thread_metadata(request.thread_id, title=request.new_title)
252
+
253
+ return RenameThreadResultSuccess(
254
+ thread_id=request.thread_id,
255
+ title=updated_meta["title"],
256
+ updated_at=updated_meta["updated_at"],
257
+ result_details="Thread renamed successfully.",
258
+ )
259
+ except Exception as e:
260
+ details = f"Error renaming thread: {e}"
261
+ logger.error(details)
262
+ return RenameThreadResultFailure(result_details=details)
263
+
264
+ def on_handle_archive_thread_request(self, request: ArchiveThreadRequest) -> ResultPayload:
265
+ try:
266
+ if not self.thread_storage_driver.thread_exists(request.thread_id):
267
+ details = f"Thread {request.thread_id} not found"
268
+ logger.error(details)
269
+ return ArchiveThreadResultFailure(result_details=details)
270
+
271
+ meta = self.thread_storage_driver.get_thread_metadata(request.thread_id)
272
+ if meta.get("archived", False):
273
+ details = f"Thread {request.thread_id} is already archived"
274
+ logger.error(details)
275
+ return ArchiveThreadResultFailure(result_details=details)
276
+
277
+ updated_meta = self.thread_storage_driver.update_thread_metadata(request.thread_id, archived=True)
278
+
279
+ return ArchiveThreadResultSuccess(
280
+ thread_id=request.thread_id,
281
+ updated_at=updated_meta["updated_at"],
282
+ result_details="Thread archived successfully.",
283
+ )
284
+ except Exception as e:
285
+ details = f"Error archiving thread: {e}"
286
+ logger.error(details)
287
+ return ArchiveThreadResultFailure(result_details=details)
288
+
289
+ def on_handle_unarchive_thread_request(self, request: UnarchiveThreadRequest) -> ResultPayload:
290
+ try:
291
+ if not self.thread_storage_driver.thread_exists(request.thread_id):
292
+ details = f"Thread {request.thread_id} not found"
293
+ logger.error(details)
294
+ return UnarchiveThreadResultFailure(result_details=details)
295
+
296
+ meta = self.thread_storage_driver.get_thread_metadata(request.thread_id)
297
+ if not meta.get("archived", False):
298
+ details = f"Thread {request.thread_id} is not archived"
299
+ logger.error(details)
300
+ return UnarchiveThreadResultFailure(result_details=details)
301
+
302
+ updated_meta = self.thread_storage_driver.update_thread_metadata(request.thread_id, archived=False)
303
+
304
+ return UnarchiveThreadResultSuccess(
305
+ thread_id=request.thread_id,
306
+ updated_at=updated_meta["updated_at"],
307
+ result_details="Thread unarchived successfully.",
308
+ )
309
+ except Exception as e:
310
+ details = f"Error unarchiving thread: {e}"
311
+ logger.error(details)
312
+ return UnarchiveThreadResultFailure(result_details=details)
313
+
314
+ def on_app_initialization_complete(self, _payload: AppInitializationComplete) -> None:
315
+ secrets_manager = GriptapeNodes.SecretsManager()
316
+ api_key = secrets_manager.get_secret("GT_CLOUD_API_KEY")
317
+ # Start MCP server in daemon thread
318
+ threading.Thread(target=start_mcp_server, args=(api_key,), daemon=True, name="mcp-server").start()
319
+
320
+ def _on_handle_run_agent_request(self, request: RunAgentRequest) -> ResultPayload:
321
+ # EventBus functionality removed - events now go directly to event queue
322
+ try:
323
+ # Get or create thread and validate
324
+ try:
325
+ thread_id = self._validate_thread_for_run(request.thread_id)
326
+ except ValueError as e:
327
+ details = str(e)
328
+ return RunAgentResultFailure(error={"message": details}, result_details=details)
329
+
330
+ # Check if this is the first run
331
+ driver = self.thread_storage_driver.get_conversation_memory_driver(thread_id)
332
+ conversation_memory = ConversationMemory(conversation_memory_driver=driver)
333
+ is_first_run = len(conversation_memory.runs) == 0
334
+
335
+ artifacts = [
336
+ ImageLoader().parse(ImageUrlArtifact.from_dict(url_artifact).to_bytes())
337
+ for url_artifact in request.url_artifacts
338
+ if url_artifact["type"] == "ImageUrlArtifact"
339
+ ]
340
+ agent = self._create_agent(thread_id=thread_id, additional_mcp_servers=request.additional_mcp_servers)
341
+ event_stream = agent.run_stream([request.input, *artifacts])
342
+ self._process_agent_stream(event_stream)
343
+
344
+ if isinstance(agent.output, ErrorArtifact):
345
+ return RunAgentResultFailure(error=agent.output.to_dict(), result_details=agent.output.to_json())
346
+
347
+ # Auto-generate title from first message if needed
348
+ if is_first_run:
349
+ title = self._generate_title_from_input(request.input)
350
+ self.thread_storage_driver.update_thread_metadata(thread_id, title=title)
351
+ else:
352
+ # Just update the timestamp
353
+ self.thread_storage_driver.update_thread_metadata(thread_id)
354
+
355
+ return RunAgentResultSuccess(
356
+ output=agent.output.to_dict(),
357
+ thread_id=thread_id,
358
+ result_details="Agent execution completed successfully.",
359
+ )
360
+ except Exception as e:
361
+ err_msg = f"Error running agent: {e}"
362
+ logger.exception(err_msg)
363
+ return RunAgentResultFailure(error=ErrorArtifact(e).to_dict(), result_details=err_msg)
364
+
365
+ def _create_agent(self, thread_id: str, additional_mcp_servers: list[str] | None = None) -> Agent:
366
+ output_schema = create_model(
367
+ "AgentOutputSchema",
368
+ conversation_output=(str, ...),
369
+ generated_image_urls=(list[str], ...),
370
+ )
371
+
372
+ tools = []
373
+ if self.image_tool is not None:
374
+ tools.append(self.image_tool)
375
+ if self.mcp_tool is not None:
376
+ tools.append(self.mcp_tool)
377
+
378
+ # Add additional MCP servers if specified
379
+ if additional_mcp_servers:
380
+ additional_tools = self._create_additional_mcp_tools(additional_mcp_servers)
381
+ tools.extend(additional_tools)
382
+
383
+ # Get thread-specific conversation memory
384
+ driver = self.thread_storage_driver.get_conversation_memory_driver(thread_id)
385
+ conversation_memory = ConversationMemory(conversation_memory_driver=driver)
386
+
387
+ return Agent(
388
+ prompt_driver=self.prompt_driver,
389
+ conversation_memory=conversation_memory,
390
+ tools=tools,
391
+ output_schema=output_schema,
392
+ rulesets=[
393
+ Ruleset(
394
+ name="generated_image_urls",
395
+ rules=[
396
+ Rule("Do not hallucinate generated_image_urls."),
397
+ Rule("Only set generated_image_urls with images generated with your tools."),
398
+ ],
399
+ ),
400
+ ],
401
+ )
402
+
403
+ def _validate_thread_for_run(self, thread_id: str | None) -> str:
404
+ """Validate and return thread_id for agent run, or raise ValueError."""
405
+ if thread_id is None:
406
+ thread_id, _ = self.thread_storage_driver.create_thread()
407
+ return thread_id
408
+
409
+ meta = self.thread_storage_driver.get_thread_metadata(thread_id)
410
+ if meta.get("archived", False):
411
+ details = f"Cannot run agent on archived thread {thread_id}. Unarchive it first."
412
+ logger.error(details)
413
+ raise ValueError(details)
414
+
415
+ return thread_id
416
+
417
+ def _process_agent_stream(self, event_stream: Iterator) -> None:
418
+ """Process agent stream events and emit streaming tokens."""
419
+ full_result = ""
420
+ last_conversation_output = ""
421
+ for event in event_stream:
422
+ if isinstance(event, TextChunkEvent):
423
+ full_result += event.token
424
+ try:
425
+ result_json = json.loads(repair_json(full_result))
426
+
427
+ if isinstance(result_json, dict) and "conversation_output" in result_json:
428
+ new_conversation_output = result_json["conversation_output"]
429
+ if new_conversation_output != last_conversation_output:
430
+ GriptapeNodes.EventManager().put_event(
431
+ ExecutionGriptapeNodeEvent(
432
+ wrapped_event=ExecutionEvent(
433
+ payload=AgentStreamEvent(
434
+ token=new_conversation_output[len(last_conversation_output) :]
435
+ )
436
+ )
437
+ )
438
+ )
439
+ last_conversation_output = new_conversation_output
440
+ except json.JSONDecodeError:
441
+ pass # Ignore incomplete JSON
442
+
128
443
  def _initialize_prompt_driver(self) -> GriptapeCloudPromptDriver:
129
444
  api_key = secrets_manager.get_secret(API_KEY_ENV_VAR)
130
445
  if not api_key:
@@ -149,6 +464,15 @@ class AgentManager:
149
464
  }
150
465
  return MCPTool(connection=connection, name="mcpGriptapeNodes")
151
466
 
467
+ def _initialize_thread_storage_driver(self) -> LocalThreadStorageDriver | GriptapeCloudThreadStorageDriver:
468
+ """Initialize the appropriate thread storage driver based on configuration."""
469
+ storage_backend = config_manager.get_config_value("thread_storage_backend")
470
+
471
+ if storage_backend == "gtc":
472
+ return GriptapeCloudThreadStorageDriver(config_manager, secrets_manager)
473
+
474
+ return LocalThreadStorageDriver(self._threads_dir, config_manager, secrets_manager)
475
+
152
476
  def _create_additional_mcp_tools(self, server_names: list[str]) -> list[MCPTool]:
153
477
  """Create MCP tools for additional servers specified in the request."""
154
478
  additional_tools = []
@@ -195,133 +519,9 @@ class AgentManager:
195
519
 
196
520
  return connection
197
521
 
198
- async def on_handle_run_agent_request(self, request: RunAgentRequest) -> ResultPayload:
199
- if self.prompt_driver is None:
200
- self.prompt_driver = self._initialize_prompt_driver()
201
- if self.image_tool is None:
202
- self.image_tool = self._initialize_image_tool()
203
- if self.mcp_tool is None:
204
- self.mcp_tool = self._initialize_mcp_tool()
205
- try:
206
- return await asyncio.to_thread(self._on_handle_run_agent_request, request)
207
- except Exception as e:
208
- err_msg = f"Error handling run agent request: {e}"
209
- return RunAgentResultFailure(error=ErrorArtifact(e).to_dict(), result_details=err_msg)
210
-
211
- def _create_agent(self, additional_mcp_servers: list[str] | None = None) -> Agent:
212
- output_schema = create_model(
213
- "AgentOutputSchema",
214
- conversation_output=(str, ...),
215
- generated_image_urls=(list[str], ...),
216
- )
217
-
218
- tools = []
219
- if self.image_tool is not None:
220
- tools.append(self.image_tool)
221
- if self.mcp_tool is not None:
222
- tools.append(self.mcp_tool)
223
-
224
- # Add additional MCP servers if specified
225
- if additional_mcp_servers:
226
- additional_tools = self._create_additional_mcp_tools(additional_mcp_servers)
227
- tools.extend(additional_tools)
228
-
229
- return Agent(
230
- prompt_driver=self.prompt_driver,
231
- conversation_memory=self.conversation_memory,
232
- tools=tools,
233
- output_schema=output_schema,
234
- rulesets=[
235
- Ruleset(
236
- name="generated_image_urls",
237
- rules=[
238
- Rule("Do not hallucinate generated_image_urls."),
239
- Rule("Only set generated_image_urls with images generated with your tools."),
240
- ],
241
- ),
242
- ],
243
- )
244
-
245
- def _on_handle_run_agent_request(self, request: RunAgentRequest) -> ResultPayload:
246
- # EventBus functionality removed - events now go directly to event queue
247
- try:
248
- artifacts = [
249
- ImageLoader().parse(ImageUrlArtifact.from_dict(url_artifact).to_bytes())
250
- for url_artifact in request.url_artifacts
251
- if url_artifact["type"] == "ImageUrlArtifact"
252
- ]
253
- agent = self._create_agent(additional_mcp_servers=request.additional_mcp_servers)
254
- event_stream = agent.run_stream([request.input, *artifacts])
255
- full_result = ""
256
- last_conversation_output = ""
257
- for event in event_stream:
258
- if isinstance(event, TextChunkEvent):
259
- full_result += event.token
260
- try:
261
- result_json = json.loads(repair_json(full_result))
262
-
263
- if isinstance(result_json, dict) and "conversation_output" in result_json:
264
- new_conversation_output = result_json["conversation_output"]
265
- if new_conversation_output != last_conversation_output:
266
- GriptapeNodes.EventManager().put_event(
267
- ExecutionGriptapeNodeEvent(
268
- wrapped_event=ExecutionEvent(
269
- payload=AgentStreamEvent(
270
- token=new_conversation_output[len(last_conversation_output) :]
271
- )
272
- )
273
- )
274
- )
275
- last_conversation_output = new_conversation_output
276
- except json.JSONDecodeError:
277
- pass # Ignore incomplete JSON
278
- if isinstance(agent.output, ErrorArtifact):
279
- return RunAgentResultFailure(error=agent.output.to_dict(), result_details=agent.output.to_json())
280
-
281
- return RunAgentResultSuccess(
282
- agent.output.to_dict(), result_details="Agent execution completed successfully."
283
- )
284
- except Exception as e:
285
- err_msg = f"Error running agent: {e}"
286
- logger.exception(err_msg)
287
- return RunAgentResultFailure(error=ErrorArtifact(e).to_dict(), result_details=err_msg)
522
+ def _generate_title_from_input(self, user_input: str, max_length: int = 50) -> str:
523
+ """Generate a thread title from user input."""
524
+ if len(user_input) <= max_length:
525
+ return user_input
288
526
 
289
- def on_handle_configure_agent_request(self, request: ConfigureAgentRequest) -> ResultPayload:
290
- try:
291
- if self.prompt_driver is None:
292
- self.prompt_driver = self._initialize_prompt_driver()
293
- for key, value in request.prompt_driver.items():
294
- setattr(self.prompt_driver, key, value)
295
- except Exception as e:
296
- details = f"Error configuring agent: {e}"
297
- logger.error(details)
298
- return ConfigureAgentResultFailure(result_details=details)
299
- return ConfigureAgentResultSuccess(result_details="Agent configured successfully.")
300
-
301
- def on_handle_reset_agent_conversation_memory_request(
302
- self, _: ResetAgentConversationMemoryRequest
303
- ) -> ResultPayload:
304
- try:
305
- self.conversation_memory = ConversationMemory()
306
- except Exception as e:
307
- details = f"Error resetting agent conversation memory: {e}"
308
- logger.error(details)
309
- return ResetAgentConversationMemoryResultFailure(result_details=details)
310
- return ResetAgentConversationMemoryResultSuccess(result_details="Agent conversation memory reset successfully.")
311
-
312
- def on_handle_get_conversation_memory_request(self, _: GetConversationMemoryRequest) -> ResultPayload:
313
- try:
314
- conversation_memory = self.conversation_memory.runs
315
- except Exception as e:
316
- details = f"Error getting conversation memory: {e}"
317
- logger.error(details)
318
- return GetConversationMemoryResultFailure(result_details=details)
319
- return GetConversationMemoryResultSuccess(
320
- runs=conversation_memory, result_details="Conversation memory retrieved successfully."
321
- )
322
-
323
- def on_app_initialization_complete(self, _payload: AppInitializationComplete) -> None:
324
- secrets_manager = GriptapeNodes.SecretsManager()
325
- api_key = secrets_manager.get_secret("GT_CLOUD_API_KEY")
326
- # Start MCP server in daemon thread
327
- threading.Thread(target=start_mcp_server, args=(api_key,), daemon=True, name="mcp-server").start()
527
+ return user_input[:max_length].rsplit(" ", 1)[0] + "..."
@@ -0,0 +1 @@
1
+ """Fitness problems package for library and workflow validation issues."""
@@ -0,0 +1,59 @@
1
+ """Library fitness problems for validation and loading issues."""
2
+
3
+ from .advanced_library_load_failure_problem import AdvancedLibraryLoadFailureProblem
4
+ from .after_library_callback_problem import AfterLibraryCallbackProblem
5
+ from .before_library_callback_problem import BeforeLibraryCallbackProblem
6
+ from .create_config_category_problem import CreateConfigCategoryProblem
7
+ from .dependency_installation_failed_problem import DependencyInstallationFailedProblem
8
+ from .deprecated_node_warning_problem import DeprecatedNodeWarningProblem
9
+ from .duplicate_library_problem import DuplicateLibraryProblem
10
+ from .duplicate_node_registration_problem import DuplicateNodeRegistrationProblem
11
+ from .engine_version_error_problem import EngineVersionErrorProblem
12
+ from .insufficient_disk_space_problem import InsufficientDiskSpaceProblem
13
+ from .invalid_version_string_problem import InvalidVersionStringProblem
14
+ from .library_json_decode_problem import LibraryJsonDecodeProblem
15
+ from .library_load_exception_problem import LibraryLoadExceptionProblem
16
+ from .library_not_found_problem import LibraryNotFoundProblem
17
+ from .library_problem import LibraryProblem
18
+ from .library_schema_exception_problem import LibrarySchemaExceptionProblem
19
+ from .library_schema_validation_problem import LibrarySchemaValidationProblem
20
+ from .modified_parameters_set_deprecation_warning_problem import ModifiedParametersSetDeprecationWarningProblem
21
+ from .modified_parameters_set_removed_problem import ModifiedParametersSetRemovedProblem
22
+ from .node_class_not_base_node_problem import NodeClassNotBaseNodeProblem
23
+ from .node_class_not_found_problem import NodeClassNotFoundProblem
24
+ from .node_module_import_problem import NodeModuleImportProblem
25
+ from .sandbox_directory_missing_problem import SandboxDirectoryMissingProblem
26
+ from .ui_options_field_modified_incompatible_problem import UiOptionsFieldModifiedIncompatibleProblem
27
+ from .ui_options_field_modified_warning_problem import UiOptionsFieldModifiedWarningProblem
28
+ from .update_config_category_problem import UpdateConfigCategoryProblem
29
+ from .venv_creation_failed_problem import VenvCreationFailedProblem
30
+
31
+ __all__ = [
32
+ "AdvancedLibraryLoadFailureProblem",
33
+ "AfterLibraryCallbackProblem",
34
+ "BeforeLibraryCallbackProblem",
35
+ "CreateConfigCategoryProblem",
36
+ "DependencyInstallationFailedProblem",
37
+ "DeprecatedNodeWarningProblem",
38
+ "DuplicateLibraryProblem",
39
+ "DuplicateNodeRegistrationProblem",
40
+ "EngineVersionErrorProblem",
41
+ "InsufficientDiskSpaceProblem",
42
+ "InvalidVersionStringProblem",
43
+ "LibraryJsonDecodeProblem",
44
+ "LibraryLoadExceptionProblem",
45
+ "LibraryNotFoundProblem",
46
+ "LibraryProblem",
47
+ "LibrarySchemaExceptionProblem",
48
+ "LibrarySchemaValidationProblem",
49
+ "ModifiedParametersSetDeprecationWarningProblem",
50
+ "ModifiedParametersSetRemovedProblem",
51
+ "NodeClassNotBaseNodeProblem",
52
+ "NodeClassNotFoundProblem",
53
+ "NodeModuleImportProblem",
54
+ "SandboxDirectoryMissingProblem",
55
+ "UiOptionsFieldModifiedIncompatibleProblem",
56
+ "UiOptionsFieldModifiedWarningProblem",
57
+ "UpdateConfigCategoryProblem",
58
+ "VenvCreationFailedProblem",
59
+ ]
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from dataclasses import dataclass
5
+
6
+ from griptape_nodes.retained_mode.managers.fitness_problems.libraries.library_problem import LibraryProblem
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ @dataclass
12
+ class AdvancedLibraryLoadFailureProblem(LibraryProblem):
13
+ """Problem indicating an advanced library module failed to load."""
14
+
15
+ advanced_library_path: str
16
+ error_message: str
17
+
18
+ @classmethod
19
+ def collate_problems_for_display(cls, instances: list[AdvancedLibraryLoadFailureProblem]) -> str:
20
+ """Display advanced library load failure problem.
21
+
22
+ There should only be one instance per library since each LibraryInfo
23
+ is already associated with a specific library path.
24
+ """
25
+ if len(instances) > 1:
26
+ logger.error(
27
+ "AdvancedLibraryLoadFailureProblem: Expected 1 instance but got %s. Each LibraryInfo should only have one AdvancedLibraryLoadFailureProblem.",
28
+ len(instances),
29
+ )
30
+
31
+ # Use the first instance's details
32
+ problem = instances[0]
33
+ return f"Failed to load Advanced Library module from '{problem.advanced_library_path}': {problem.error_message}"
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from dataclasses import dataclass
5
+
6
+ from griptape_nodes.retained_mode.managers.fitness_problems.libraries.library_problem import LibraryProblem
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ @dataclass
12
+ class AfterLibraryCallbackProblem(LibraryProblem):
13
+ """Problem indicating an error calling the after_library_nodes_loaded callback."""
14
+
15
+ error_message: str
16
+
17
+ @classmethod
18
+ def collate_problems_for_display(cls, instances: list[AfterLibraryCallbackProblem]) -> str:
19
+ """Display after library callback problem.
20
+
21
+ There should only be one instance per library since there's only one
22
+ after_library_nodes_loaded callback per library.
23
+ """
24
+ if len(instances) > 1:
25
+ logger.error(
26
+ "AfterLibraryCallbackProblem: Expected 1 instance but got %s. Each LibraryInfo should only have one AfterLibraryCallbackProblem.",
27
+ len(instances),
28
+ )
29
+
30
+ # Use the first instance's error message
31
+ error_msg = instances[0].error_message
32
+ return f"Error calling after_library_nodes_loaded callback: {error_msg}"