admin.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. from __future__ import annotations
  2. from sqlalchemy import text
  3. def knowledge_status(session) -> dict:
  4. change_sets = session.execute(
  5. text(
  6. """
  7. SELECT status, COUNT(*)
  8. FROM public.knowledge_change_sets
  9. GROUP BY status
  10. """
  11. )
  12. ).all()
  13. projections = session.execute(
  14. text(
  15. """
  16. SELECT engine, status, COUNT(*)
  17. FROM public.knowledge_index_projections
  18. GROUP BY engine, status
  19. """
  20. )
  21. ).all()
  22. freshness = session.execute(
  23. text(
  24. """
  25. SELECT COUNT(*) FILTER (WHERE status = 'active') AS active_documents,
  26. MAX(source_updated_at) FILTER (WHERE status = 'active') AS latest_source
  27. FROM public.governance_documents
  28. """
  29. )
  30. ).one()
  31. return {
  32. "change_sets": {row[0]: row[1] for row in change_sets},
  33. "projections": {f"{row[0]}:{row[1]}": row[2] for row in projections},
  34. "active_documents": freshness[0],
  35. "latest_source_updated_at": (
  36. freshness[1].isoformat() if freshness[1] else None
  37. ),
  38. }
  39. def list_change_sets(session, *, limit: int = 50) -> list[dict]:
  40. rows = session.execute(
  41. text(
  42. """
  43. SELECT id::text, source_type, source_uid::text, source_revision,
  44. change_type, added_count, modified_count, deleted_count,
  45. impacted_point_count, impact_truncated, status, target_generation,
  46. last_error, created_at, activated_at
  47. FROM public.knowledge_change_sets
  48. ORDER BY created_at DESC
  49. LIMIT :limit
  50. """
  51. ),
  52. {"limit": limit},
  53. ).mappings()
  54. return [dict(row) for row in rows]
  55. def change_set_detail(session, change_set_id: str) -> dict | None:
  56. change_set = (
  57. session.execute(
  58. text(
  59. """
  60. SELECT id::text, source_type, source_uid::text, source_revision,
  61. change_type, source_snapshot_hash, added_count, modified_count,
  62. deleted_count, impacted_point_count, impact_truncated, status,
  63. target_generation, last_error, created_at, updated_at, activated_at
  64. FROM public.knowledge_change_sets
  65. WHERE id = CAST(:id AS uuid)
  66. """
  67. ),
  68. {"id": change_set_id},
  69. )
  70. .mappings()
  71. .one_or_none()
  72. )
  73. if change_set is None:
  74. return None
  75. items = session.execute(
  76. text(
  77. """
  78. SELECT point_key, change_kind, old_point_revision, new_point_revision,
  79. old_content_hash, new_content_hash, caused_by_point_key,
  80. propagation_hop, canonical_status, embedding_status,
  81. cache_status, lightrag_status, attempts, last_error
  82. FROM public.knowledge_change_items
  83. WHERE change_set_id = CAST(:id AS uuid)
  84. ORDER BY point_key
  85. """
  86. ),
  87. {"id": change_set_id},
  88. ).mappings()
  89. return {**dict(change_set), "items": [dict(row) for row in items]}
  90. def retry_change_set(session, change_set_id: str) -> bool:
  91. updated = session.execute(
  92. text(
  93. """
  94. UPDATE public.knowledge_change_sets
  95. SET status = 'pending', last_error = NULL, updated_at = CURRENT_TIMESTAMP
  96. WHERE id = CAST(:id AS uuid) AND status IN ('failed','degraded')
  97. """
  98. ),
  99. {"id": change_set_id},
  100. ).rowcount
  101. if updated:
  102. session.execute(
  103. text(
  104. """
  105. UPDATE public.knowledge_index_projections
  106. SET status = 'pending', last_error = NULL, updated_at = CURRENT_TIMESTAMP
  107. WHERE change_set_id = CAST(:id AS uuid) AND status IN ('failed','unverified')
  108. """
  109. ),
  110. {"id": change_set_id},
  111. )
  112. return updated == 1
  113. def rollback_change_set(session, change_set_id: str) -> bool:
  114. current = session.execute(
  115. text(
  116. """
  117. SELECT d.id::text, d.object_uid::text, d.object_version
  118. FROM public.governance_documents d
  119. JOIN public.knowledge_change_sets cs ON cs.id = d.change_set_id
  120. WHERE cs.id = CAST(:id AS uuid)
  121. AND cs.status = 'canonical_active'
  122. AND d.status = 'active'
  123. FOR UPDATE
  124. """
  125. ),
  126. {"id": change_set_id},
  127. ).one_or_none()
  128. if current is None:
  129. return False
  130. previous = session.execute(
  131. text(
  132. """
  133. SELECT id::text, object_version
  134. FROM public.governance_documents
  135. WHERE object_uid = CAST(:uid AS uuid)
  136. AND object_version < :version
  137. ORDER BY object_version DESC
  138. LIMIT 1
  139. FOR UPDATE
  140. """
  141. ),
  142. {"uid": current[1], "version": current[2]},
  143. ).one_or_none()
  144. if previous is None:
  145. return False
  146. session.execute(
  147. text(
  148. """
  149. UPDATE public.governance_documents SET status = 'superseded'
  150. WHERE id = CAST(:id AS uuid);
  151. UPDATE public.governance_documents SET status = 'active'
  152. WHERE id = CAST(:previous_id AS uuid);
  153. UPDATE public.knowledge_points SET status = 'superseded',
  154. valid_to = CURRENT_TIMESTAMP
  155. WHERE source_uid = CAST(:uid AS uuid) AND status = 'active';
  156. WITH restore AS (
  157. SELECT DISTINCT ON (point_key) id
  158. FROM public.knowledge_points
  159. WHERE source_uid = CAST(:uid AS uuid)
  160. AND source_revision <= :previous_version
  161. AND status <> 'deleted'
  162. ORDER BY point_key, point_revision DESC
  163. )
  164. UPDATE public.knowledge_points p
  165. SET status = 'active', valid_to = NULL, activated_at = CURRENT_TIMESTAMP
  166. FROM restore WHERE p.id = restore.id;
  167. UPDATE public.knowledge_change_sets
  168. SET status = 'rolled_back', updated_at = CURRENT_TIMESTAMP
  169. WHERE id = CAST(:change_set_id AS uuid);
  170. UPDATE public.knowledge_index_projections
  171. SET status = 'deleting', updated_at = CURRENT_TIMESTAMP
  172. WHERE document_id = CAST(:id AS uuid) AND status <> 'deleted';
  173. """
  174. ),
  175. {
  176. "id": current[0],
  177. "previous_id": previous[0],
  178. "uid": current[1],
  179. "previous_version": previous[1],
  180. "change_set_id": change_set_id,
  181. },
  182. )
  183. return True