event.py 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. from dataclasses import asdict
  2. from datetime import datetime
  3. from openhands.events import Event, EventSource
  4. from openhands.events.observation.observation import Observation
  5. from openhands.events.serialization.action import action_from_dict
  6. from openhands.events.serialization.observation import observation_from_dict
  7. from openhands.events.serialization.utils import remove_fields
  8. # TODO: move `content` into `extras`
  9. TOP_KEYS = ['id', 'timestamp', 'source', 'message', 'cause', 'action', 'observation']
  10. UNDERSCORE_KEYS = ['id', 'timestamp', 'source', 'cause']
  11. DELETE_FROM_MEMORY_EXTRAS = {
  12. 'screenshot',
  13. 'dom_object',
  14. 'axtree_object',
  15. 'open_pages_urls',
  16. 'active_page_index',
  17. 'last_browser_action',
  18. 'last_browser_action_error',
  19. 'focused_element_bid',
  20. 'extra_element_properties',
  21. }
  22. def event_from_dict(data) -> 'Event':
  23. evt: Event
  24. if 'action' in data:
  25. evt = action_from_dict(data)
  26. elif 'observation' in data:
  27. evt = observation_from_dict(data)
  28. else:
  29. raise ValueError('Unknown event type: ' + data)
  30. for key in UNDERSCORE_KEYS:
  31. if key in data:
  32. value = data[key]
  33. if key == 'timestamp':
  34. value = datetime.fromisoformat(value)
  35. if key == 'source':
  36. value = EventSource(value)
  37. setattr(evt, '_' + key, value)
  38. return evt
  39. def event_to_dict(event: 'Event') -> dict:
  40. props = asdict(event)
  41. d = {}
  42. for key in TOP_KEYS:
  43. if hasattr(event, key) and getattr(event, key) is not None:
  44. d[key] = getattr(event, key)
  45. elif hasattr(event, f'_{key}') and getattr(event, f'_{key}') is not None:
  46. d[key] = getattr(event, f'_{key}')
  47. if key == 'id' and d.get('id') == -1:
  48. d.pop('id', None)
  49. if key == 'timestamp' and 'timestamp' in d:
  50. d['timestamp'] = d['timestamp'].isoformat()
  51. if key == 'source' and 'source' in d:
  52. d['source'] = d['source'].value
  53. props.pop(key, None)
  54. if 'security_risk' in props and props['security_risk'] is None:
  55. props.pop('security_risk')
  56. if 'action' in d:
  57. d['args'] = props
  58. if event.timeout is not None:
  59. d['timeout'] = event.timeout
  60. elif 'observation' in d:
  61. d['content'] = props.pop('content', '')
  62. d['extras'] = props
  63. else:
  64. raise ValueError('Event must be either action or observation')
  65. return d
  66. def event_to_memory(event: 'Event', max_message_chars: int) -> dict:
  67. d = event_to_dict(event)
  68. d.pop('id', None)
  69. d.pop('cause', None)
  70. d.pop('timestamp', None)
  71. d.pop('message', None)
  72. d.pop('images_urls', None)
  73. if 'extras' in d:
  74. remove_fields(d['extras'], DELETE_FROM_MEMORY_EXTRAS)
  75. if isinstance(event, Observation) and 'content' in d:
  76. d['content'] = truncate_content(d['content'], max_message_chars)
  77. return d
  78. def truncate_content(content: str, max_chars: int) -> str:
  79. """Truncate the middle of the observation content if it is too long."""
  80. if len(content) <= max_chars:
  81. return content
  82. # truncate the middle and include a message to the LLM about it
  83. half = max_chars // 2
  84. return (
  85. content[:half]
  86. + '\n[... Observation truncated due to length ...]\n'
  87. + content[-half:]
  88. )