repository.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. from __future__ import annotations
  2. from datetime import date, datetime
  3. from typing import Any
  4. from sqlalchemy import case, func
  5. from ..runtime_bridge import load_runtime_modules, new_session
  6. def _value(value: Any) -> Any:
  7. if isinstance(value, (datetime, date)):
  8. return value.isoformat()
  9. if isinstance(value, dict):
  10. return {str(key): _value(item) for key, item in value.items()}
  11. if isinstance(value, (list, tuple)):
  12. return [_value(item) for item in value]
  13. return value
  14. def _row(row: Any) -> dict[str, Any]:
  15. if row is None:
  16. return {}
  17. mapping = getattr(row, "_mapping", None)
  18. if mapping is not None:
  19. return {str(key): _value(value) for key, value in mapping.items()}
  20. return {
  21. column.name: _value(getattr(row, column.name))
  22. for column in row.__table__.columns
  23. }
  24. class AuditRepository:
  25. """Loads only audit-only full records; every query is read-only."""
  26. def load_event(self, script_build_id: int, event_id: int) -> dict[str, Any] | None:
  27. _, models = load_runtime_modules()
  28. session = new_session()
  29. try:
  30. event = (
  31. session.query(models.ScriptBuildEvent)
  32. .filter(
  33. models.ScriptBuildEvent.script_build_id == script_build_id,
  34. models.ScriptBuildEvent.id == event_id,
  35. )
  36. .first()
  37. )
  38. if event is None:
  39. return None
  40. body = (
  41. session.query(models.ScriptBuildEventBody)
  42. .filter(
  43. models.ScriptBuildEventBody.script_build_id == script_build_id,
  44. models.ScriptBuildEventBody.event_id == event_id,
  45. )
  46. .first()
  47. )
  48. payload = _row(event)
  49. if body is not None:
  50. body_row = _row(body)
  51. payload["eventBody"] = {
  52. "id": body_row.get("id"),
  53. "input_content_type": body_row.get("input_content_type"),
  54. "input_content": body_row.get("input_content"),
  55. "output_content_type": body_row.get("output_content_type"),
  56. "output_content": body_row.get("output_content"),
  57. "created_at": body_row.get("created_at"),
  58. "updated_at": body_row.get("updated_at"),
  59. }
  60. return payload
  61. finally:
  62. session.close()
  63. def load_log(self, script_build_id: int) -> str | None:
  64. _, models = load_runtime_modules()
  65. session = new_session()
  66. try:
  67. row = (
  68. session.query(models.ScriptBuildLog)
  69. .filter(models.ScriptBuildLog.script_build_id == script_build_id)
  70. .first()
  71. )
  72. return str(row.log_content) if row and row.log_content is not None else None
  73. finally:
  74. session.close()
  75. def load_event_log_windows(
  76. self,
  77. script_build_id: int,
  78. event_anchors: list[tuple[int, str, int | None]],
  79. *,
  80. window_size: int = 50_000,
  81. ) -> dict[int, str]:
  82. """Read only anchored log windows in one query, never the whole Run log."""
  83. # `script_build_event.event_seq` is the structured event stream order,
  84. # while the seq stored in script_build_log anchors is the model-message
  85. # stream order. They are not interchangeable. Only an exact msg_id is
  86. # safe enough for the source Inspector.
  87. unique = [
  88. item
  89. for item in dict.fromkeys(event_anchors)
  90. if str(item[1] or "").strip()
  91. ]
  92. if not unique:
  93. return {}
  94. _, models = load_runtime_modules()
  95. session = new_session()
  96. try:
  97. content = models.ScriptBuildLog.log_content
  98. expressions = []
  99. for _event_id, msg_id, _event_seq in unique:
  100. position = func.greatest(
  101. func.locate(f"[MSG_ANCHOR:msg_id={msg_id}", content),
  102. func.locate(f"[TOOL_ANCHOR:msg_id={msg_id}", content),
  103. )
  104. start = func.greatest(position - 2_000, 1)
  105. expressions.append(
  106. case(
  107. (position > 0, func.substr(content, start, window_size)),
  108. else_=None,
  109. )
  110. )
  111. row = (
  112. session.query(*expressions)
  113. .filter(models.ScriptBuildLog.script_build_id == script_build_id)
  114. .first()
  115. )
  116. if row is None:
  117. return {}
  118. return {
  119. event_id: str(row[index])
  120. for index, (event_id, _msg_id, _event_seq) in enumerate(unique)
  121. if row[index] is not None
  122. }
  123. finally:
  124. session.close()