test_memory.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. import json
  2. from datetime import datetime, timezone
  3. from unittest.mock import MagicMock, patch
  4. import pytest
  5. from openhands.core.config import AgentConfig, LLMConfig
  6. from openhands.events.event import Event, EventSource
  7. from openhands.events.stream import EventStream
  8. from openhands.memory.memory import LongTermMemory
  9. from openhands.storage.files import FileStore
  10. @pytest.fixture
  11. def mock_llm_config() -> LLMConfig:
  12. config = MagicMock(spec=LLMConfig)
  13. config.embedding_model = 'test_embedding_model'
  14. config.api_key = 'test_api_key'
  15. config.api_version = 'v1'
  16. return config
  17. @pytest.fixture
  18. def mock_agent_config() -> AgentConfig:
  19. config = AgentConfig(
  20. micro_agent_name='test_micro_agent',
  21. memory_enabled=True,
  22. memory_max_threads=4,
  23. llm_config='test_llm_config',
  24. )
  25. return config
  26. @pytest.fixture
  27. def mock_file_store() -> FileStore:
  28. store = MagicMock(spec=FileStore)
  29. store.sid = 'test_session'
  30. return store
  31. @pytest.fixture
  32. def mock_event_stream(mock_file_store: FileStore) -> EventStream:
  33. with patch('openhands.events.stream.EventStream') as MockEventStream:
  34. instance = MockEventStream.return_value
  35. instance.sid = 'test_session'
  36. instance.get_events = MagicMock()
  37. return instance
  38. @pytest.fixture
  39. def long_term_memory(
  40. mock_llm_config: LLMConfig,
  41. mock_agent_config: AgentConfig,
  42. mock_event_stream: EventStream,
  43. ) -> LongTermMemory:
  44. mod = LongTermMemory.__module__
  45. with patch(f'{mod}.chromadb.PersistentClient') as mock_chroma_client:
  46. mock_collection = MagicMock()
  47. mock_chroma_client.return_value.get_or_create_collection.return_value = (
  48. mock_collection
  49. )
  50. with (
  51. patch(f'{mod}.ChromaVectorStore', MagicMock()),
  52. patch(f'{mod}.EmbeddingsLoader', MagicMock()),
  53. patch(f'{mod}.VectorStoreIndex', MagicMock()),
  54. ):
  55. memory = LongTermMemory(
  56. llm_config=mock_llm_config,
  57. agent_config=mock_agent_config,
  58. event_stream=mock_event_stream,
  59. )
  60. memory.collection = mock_collection
  61. return memory
  62. def _create_action_event(action: str) -> Event:
  63. """Helper function to create an action event."""
  64. event = Event()
  65. event._id = -1
  66. event._timestamp = datetime.now(timezone.utc).isoformat()
  67. event._source = EventSource.AGENT
  68. event.action = action
  69. return event
  70. def _create_observation_event(observation: str) -> Event:
  71. """Helper function to create an observation event."""
  72. event = Event()
  73. event._id = -1
  74. event._timestamp = datetime.now(timezone.utc).isoformat()
  75. event._source = EventSource.ENVIRONMENT
  76. event.observation = observation
  77. return event
  78. def test_add_event_with_action(long_term_memory: LongTermMemory):
  79. event = _create_action_event('test_action')
  80. long_term_memory._add_document = MagicMock()
  81. long_term_memory.add_event(event)
  82. assert long_term_memory.thought_idx == 1
  83. long_term_memory._add_document.assert_called_once()
  84. _, kwargs = long_term_memory._add_document.call_args
  85. assert kwargs['document'].extra_info['type'] == 'action'
  86. assert kwargs['document'].extra_info['id'] == 'test_action'
  87. def test_add_event_with_observation(long_term_memory: LongTermMemory):
  88. event = _create_observation_event('test_observation')
  89. long_term_memory._add_document = MagicMock()
  90. long_term_memory.add_event(event)
  91. assert long_term_memory.thought_idx == 1
  92. long_term_memory._add_document.assert_called_once()
  93. _, kwargs = long_term_memory._add_document.call_args
  94. assert kwargs['document'].extra_info['type'] == 'observation'
  95. assert kwargs['document'].extra_info['id'] == 'test_observation'
  96. def test_add_event_with_missing_keys(long_term_memory: LongTermMemory):
  97. # Creating an event with additional unexpected attributes
  98. event = Event()
  99. event._id = -1
  100. event._timestamp = datetime.now(timezone.utc).isoformat()
  101. event._source = EventSource.AGENT
  102. event.action = 'test_action'
  103. event.unexpected_key = 'value'
  104. long_term_memory._add_document = MagicMock()
  105. long_term_memory.add_event(event)
  106. assert long_term_memory.thought_idx == 1
  107. long_term_memory._add_document.assert_called_once()
  108. _, kwargs = long_term_memory._add_document.call_args
  109. assert kwargs['document'].extra_info['type'] == 'action'
  110. assert kwargs['document'].extra_info['id'] == 'test_action'
  111. def test_events_to_docs_no_events(
  112. long_term_memory: LongTermMemory, mock_event_stream: EventStream
  113. ):
  114. mock_event_stream.get_events.side_effect = FileNotFoundError
  115. # convert events to documents
  116. documents = long_term_memory._events_to_docs()
  117. # since get_events raises, documents should be empty
  118. assert len(documents) == 0
  119. # thought_idx remains unchanged
  120. assert long_term_memory.thought_idx == 0
  121. def test_load_events_into_index_with_invalid_json(
  122. long_term_memory: LongTermMemory, mock_event_stream: EventStream
  123. ):
  124. """Test loading events with malformed event data."""
  125. # Simulate an event that causes event_to_memory to raise a JSONDecodeError
  126. with patch(
  127. 'openhands.memory.memory.event_to_memory',
  128. side_effect=json.JSONDecodeError('Expecting value', '', 0),
  129. ):
  130. event = _create_action_event('invalid_action')
  131. mock_event_stream.get_events.return_value = [event]
  132. # convert events to documents
  133. documents = long_term_memory._events_to_docs()
  134. # since event_to_memory raises, documents should be empty
  135. assert len(documents) == 0
  136. # thought_idx remains unchanged
  137. assert long_term_memory.thought_idx == 0
  138. def test_embeddings_inserted_into_chroma(long_term_memory: LongTermMemory):
  139. event = _create_action_event('test_action')
  140. long_term_memory._add_document = MagicMock()
  141. long_term_memory.add_event(event)
  142. long_term_memory._add_document.assert_called()
  143. _, kwargs = long_term_memory._add_document.call_args
  144. assert 'document' in kwargs
  145. assert (
  146. kwargs['document'].text
  147. == '{"source": "agent", "action": "test_action", "args": {}}'
  148. )
  149. def test_search_returns_correct_results(long_term_memory: LongTermMemory):
  150. mock_retriever = MagicMock()
  151. mock_retriever.retrieve.return_value = [
  152. MagicMock(get_text=MagicMock(return_value='result1')),
  153. MagicMock(get_text=MagicMock(return_value='result2')),
  154. ]
  155. with patch(
  156. 'openhands.memory.memory.VectorIndexRetriever', return_value=mock_retriever
  157. ):
  158. results = long_term_memory.search(query='test query', k=2)
  159. assert results == ['result1', 'result2']
  160. mock_retriever.retrieve.assert_called_once_with('test query')
  161. def test_search_with_no_results(long_term_memory: LongTermMemory):
  162. mock_retriever = MagicMock()
  163. mock_retriever.retrieve.return_value = []
  164. with patch(
  165. 'openhands.memory.memory.VectorIndexRetriever', return_value=mock_retriever
  166. ):
  167. results = long_term_memory.search(query='no results', k=5)
  168. assert results == []
  169. mock_retriever.retrieve.assert_called_once_with('no results')
  170. def test_add_event_increment_thought_idx(long_term_memory: LongTermMemory):
  171. event1 = _create_action_event('action1')
  172. event2 = _create_observation_event('observation1')
  173. long_term_memory.add_event(event1)
  174. long_term_memory.add_event(event2)
  175. assert long_term_memory.thought_idx == 2
  176. def test_load_events_batch_insert(
  177. long_term_memory: LongTermMemory, mock_event_stream: EventStream
  178. ):
  179. event1 = _create_action_event('action1')
  180. event2 = _create_observation_event('observation1')
  181. event3 = _create_action_event('action2')
  182. mock_event_stream.get_events.return_value = [event1, event2, event3]
  183. # Mock insert_batch_docs
  184. with patch('openhands.utils.embeddings.insert_batch_docs') as mock_run_docs:
  185. # convert events to documents
  186. documents = long_term_memory._events_to_docs()
  187. # Mock the insert_batch_docs to simulate document insertion
  188. mock_run_docs.return_value = []
  189. # Call insert_batch_docs with the documents
  190. mock_run_docs(
  191. index=long_term_memory.index,
  192. documents=documents,
  193. num_workers=long_term_memory.memory_max_threads,
  194. )
  195. # Assert that insert_batch_docs was called with the correct arguments
  196. mock_run_docs.assert_called_once_with(
  197. index=long_term_memory.index,
  198. documents=documents,
  199. num_workers=long_term_memory.memory_max_threads,
  200. )
  201. # Check if thought_idx was incremented correctly
  202. assert long_term_memory.thought_idx == 3