event.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  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. from openhands.events.tool import ToolCallMetadata
  9. # TODO: move `content` into `extras`
  10. TOP_KEYS = [
  11. 'id',
  12. 'timestamp',
  13. 'source',
  14. 'message',
  15. 'cause',
  16. 'action',
  17. 'observation',
  18. 'tool_call_metadata',
  19. ]
  20. UNDERSCORE_KEYS = ['id', 'timestamp', 'source', 'cause', 'tool_call_metadata']
  21. DELETE_FROM_TRAJECTORY_EXTRAS = {
  22. 'screenshot',
  23. 'dom_object',
  24. 'axtree_object',
  25. 'active_page_index',
  26. 'last_browser_action',
  27. 'last_browser_action_error',
  28. 'focused_element_bid',
  29. 'extra_element_properties',
  30. }
  31. DELETE_FROM_MEMORY_EXTRAS = DELETE_FROM_TRAJECTORY_EXTRAS | {'open_pages_urls'}
  32. def event_from_dict(data) -> 'Event':
  33. evt: Event
  34. if 'action' in data:
  35. evt = action_from_dict(data)
  36. elif 'observation' in data:
  37. evt = observation_from_dict(data)
  38. else:
  39. raise ValueError('Unknown event type: ' + data)
  40. for key in UNDERSCORE_KEYS:
  41. if key in data:
  42. value = data[key]
  43. if key == 'timestamp' and isinstance(value, datetime):
  44. value = value.isoformat()
  45. if key == 'source':
  46. value = EventSource(value)
  47. if key == 'tool_call_metadata':
  48. value = ToolCallMetadata(**value)
  49. setattr(evt, '_' + key, value)
  50. return evt
  51. def event_to_dict(event: 'Event') -> dict:
  52. props = asdict(event)
  53. d = {}
  54. for key in TOP_KEYS:
  55. if hasattr(event, key) and getattr(event, key) is not None:
  56. d[key] = getattr(event, key)
  57. elif hasattr(event, f'_{key}') and getattr(event, f'_{key}') is not None:
  58. d[key] = getattr(event, f'_{key}')
  59. if key == 'id' and d.get('id') == -1:
  60. d.pop('id', None)
  61. if key == 'timestamp' and 'timestamp' in d:
  62. if isinstance(d['timestamp'], datetime):
  63. d['timestamp'] = d['timestamp'].isoformat()
  64. if key == 'source' and 'source' in d:
  65. d['source'] = d['source'].value
  66. if key == 'tool_call_metadata' and 'tool_call_metadata' in d:
  67. d['tool_call_metadata'] = d['tool_call_metadata'].model_dump()
  68. props.pop(key, None)
  69. if 'security_risk' in props and props['security_risk'] is None:
  70. props.pop('security_risk')
  71. if 'action' in d:
  72. d['args'] = props
  73. if event.timeout is not None:
  74. d['timeout'] = event.timeout
  75. elif 'observation' in d:
  76. d['content'] = props.pop('content', '')
  77. d['extras'] = props
  78. # Include success field for CmdOutputObservation
  79. if hasattr(event, 'success'):
  80. d['success'] = event.success
  81. else:
  82. raise ValueError('Event must be either action or observation')
  83. return d
  84. def event_to_trajectory(event: 'Event') -> dict:
  85. d = event_to_dict(event)
  86. if 'extras' in d:
  87. remove_fields(d['extras'], DELETE_FROM_TRAJECTORY_EXTRAS)
  88. return d
  89. def event_to_memory(event: 'Event', max_message_chars: int) -> dict:
  90. d = event_to_dict(event)
  91. d.pop('id', None)
  92. d.pop('cause', None)
  93. d.pop('timestamp', None)
  94. d.pop('message', None)
  95. d.pop('image_urls', None)
  96. # runnable actions have some extra fields used in the BE/FE, which should not be sent to the LLM
  97. if 'args' in d:
  98. d['args'].pop('blocking', None)
  99. d['args'].pop('keep_prompt', None)
  100. d['args'].pop('confirmation_state', None)
  101. if 'extras' in d:
  102. remove_fields(d['extras'], DELETE_FROM_MEMORY_EXTRAS)
  103. if isinstance(event, Observation) and 'content' in d:
  104. d['content'] = truncate_content(d['content'], max_message_chars)
  105. return d
  106. def truncate_content(content: str, max_chars: int) -> str:
  107. """Truncate the middle of the observation content if it is too long."""
  108. if len(content) <= max_chars or max_chars == -1:
  109. return content
  110. # truncate the middle and include a message to the LLM about it
  111. half = max_chars // 2
  112. return (
  113. content[:half]
  114. + '\n[... Observation truncated due to length ...]\n'
  115. + content[-half:]
  116. )