test_memory.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  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. with patch(
  45. 'openhands.memory.memory.chromadb.PersistentClient'
  46. ) as mock_chroma_client:
  47. mock_collection = MagicMock()
  48. mock_chroma_client.return_value.get_or_create_collection.return_value = (
  49. mock_collection
  50. )
  51. memory = LongTermMemory(
  52. llm_config=mock_llm_config,
  53. agent_config=mock_agent_config,
  54. event_stream=mock_event_stream,
  55. )
  56. memory.collection = mock_collection
  57. return memory
  58. def _create_action_event(action: str) -> Event:
  59. """Helper function to create an action event."""
  60. event = Event()
  61. event._id = -1
  62. event._timestamp = datetime.now(timezone.utc).isoformat()
  63. event._source = EventSource.AGENT
  64. event.action = action
  65. return event
  66. def _create_observation_event(observation: str) -> Event:
  67. """Helper function to create an observation event."""
  68. event = Event()
  69. event._id = -1
  70. event._timestamp = datetime.now(timezone.utc).isoformat()
  71. event._source = EventSource.USER
  72. event.observation = observation
  73. return event
  74. def test_add_event_with_action(long_term_memory: LongTermMemory):
  75. event = _create_action_event('test_action')
  76. long_term_memory._add_document = MagicMock()
  77. long_term_memory.add_event(event)
  78. assert long_term_memory.thought_idx == 1
  79. long_term_memory._add_document.assert_called_once()
  80. _, kwargs = long_term_memory._add_document.call_args
  81. assert kwargs['document'].extra_info['type'] == 'action'
  82. assert kwargs['document'].extra_info['id'] == 'test_action'
  83. def test_add_event_with_observation(long_term_memory: LongTermMemory):
  84. event = _create_observation_event('test_observation')
  85. long_term_memory._add_document = MagicMock()
  86. long_term_memory.add_event(event)
  87. assert long_term_memory.thought_idx == 1
  88. long_term_memory._add_document.assert_called_once()
  89. _, kwargs = long_term_memory._add_document.call_args
  90. assert kwargs['document'].extra_info['type'] == 'observation'
  91. assert kwargs['document'].extra_info['id'] == 'test_observation'
  92. def test_add_event_with_missing_keys(long_term_memory: LongTermMemory):
  93. # Creating an event with additional unexpected attributes
  94. event = Event()
  95. event._id = -1
  96. event._timestamp = datetime.now(timezone.utc).isoformat()
  97. event._source = EventSource.AGENT
  98. event.action = 'test_action'
  99. event.unexpected_key = 'value'
  100. long_term_memory._add_document = MagicMock()
  101. long_term_memory.add_event(event)
  102. assert long_term_memory.thought_idx == 1
  103. long_term_memory._add_document.assert_called_once()
  104. _, kwargs = long_term_memory._add_document.call_args
  105. assert kwargs['document'].extra_info['type'] == 'action'
  106. assert kwargs['document'].extra_info['id'] == 'test_action'
  107. def test_events_to_docs_no_events(
  108. long_term_memory: LongTermMemory, mock_event_stream: EventStream
  109. ):
  110. mock_event_stream.get_events.side_effect = FileNotFoundError
  111. # convert events to documents
  112. documents = long_term_memory._events_to_docs()
  113. # since get_events raises, documents should be empty
  114. assert len(documents) == 0
  115. # thought_idx remains unchanged
  116. assert long_term_memory.thought_idx == 0
  117. def test_load_events_into_index_with_invalid_json(
  118. long_term_memory: LongTermMemory, mock_event_stream: EventStream
  119. ):
  120. """Test loading events with malformed event data."""
  121. # Simulate an event that causes event_to_memory to raise a JSONDecodeError
  122. with patch(
  123. 'openhands.memory.memory.event_to_memory',
  124. side_effect=json.JSONDecodeError('Expecting value', '', 0),
  125. ):
  126. event = _create_action_event('invalid_action')
  127. mock_event_stream.get_events.return_value = [event]
  128. # convert events to documents
  129. documents = long_term_memory._events_to_docs()
  130. # since event_to_memory raises, documents should be empty
  131. assert len(documents) == 0
  132. # thought_idx remains unchanged
  133. assert long_term_memory.thought_idx == 0
  134. def test_embeddings_inserted_into_chroma(long_term_memory: LongTermMemory):
  135. event = _create_action_event('test_action')
  136. long_term_memory._add_document = MagicMock()
  137. long_term_memory.add_event(event)
  138. long_term_memory._add_document.assert_called()
  139. _, kwargs = long_term_memory._add_document.call_args
  140. assert 'document' in kwargs
  141. assert (
  142. kwargs['document'].text
  143. == '{"source": "agent", "action": "test_action", "args": {}}'
  144. )
  145. def test_search_returns_correct_results(long_term_memory: LongTermMemory):
  146. mock_retriever = MagicMock()
  147. mock_retriever.retrieve.return_value = [
  148. MagicMock(get_text=MagicMock(return_value='result1')),
  149. MagicMock(get_text=MagicMock(return_value='result2')),
  150. ]
  151. with patch(
  152. 'openhands.memory.memory.VectorIndexRetriever', return_value=mock_retriever
  153. ):
  154. results = long_term_memory.search(query='test query', k=2)
  155. assert results == ['result1', 'result2']
  156. mock_retriever.retrieve.assert_called_once_with('test query')
  157. def test_search_with_no_results(long_term_memory: LongTermMemory):
  158. mock_retriever = MagicMock()
  159. mock_retriever.retrieve.return_value = []
  160. with patch(
  161. 'openhands.memory.memory.VectorIndexRetriever', return_value=mock_retriever
  162. ):
  163. results = long_term_memory.search(query='no results', k=5)
  164. assert results == []
  165. mock_retriever.retrieve.assert_called_once_with('no results')
  166. def test_add_event_increment_thought_idx(long_term_memory: LongTermMemory):
  167. event1 = _create_action_event('action1')
  168. event2 = _create_observation_event('observation1')
  169. long_term_memory.add_event(event1)
  170. long_term_memory.add_event(event2)
  171. assert long_term_memory.thought_idx == 2
  172. def test_load_events_batch_insert(
  173. long_term_memory: LongTermMemory, mock_event_stream: EventStream
  174. ):
  175. event1 = _create_action_event('action1')
  176. event2 = _create_observation_event('observation1')
  177. event3 = _create_action_event('action2')
  178. mock_event_stream.get_events.return_value = [event1, event2, event3]
  179. # Mock insert_batch_docs
  180. with patch('openhands.utils.embeddings.insert_batch_docs') as mock_run_docs:
  181. # convert events to documents
  182. documents = long_term_memory._events_to_docs()
  183. # Mock the insert_batch_docs to simulate document insertion
  184. mock_run_docs.return_value = []
  185. # Call insert_batch_docs with the documents
  186. mock_run_docs(
  187. index=long_term_memory.index,
  188. documents=documents,
  189. num_workers=long_term_memory.memory_max_threads,
  190. )
  191. # Assert that insert_batch_docs was called with the correct arguments
  192. mock_run_docs.assert_called_once_with(
  193. index=long_term_memory.index,
  194. documents=documents,
  195. num_workers=long_term_memory.memory_max_threads,
  196. )
  197. # Check if thought_idx was incremented correctly
  198. assert long_term_memory.thought_idx == 3