client.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. from __future__ import annotations
  2. import time
  3. from dataclasses import dataclass
  4. from typing import Any
  5. import requests
  6. class LightRAGCircuitOpen(RuntimeError):
  7. pass
  8. @dataclass(frozen=True)
  9. class ProjectionReceipt:
  10. track_id: str
  11. @dataclass(frozen=True)
  12. class DeleteReceipt:
  13. verified: bool
  14. class _RequestsTransport:
  15. def request(self, **kwargs):
  16. return requests.request(**kwargs)
  17. class LightRAGClient:
  18. def __init__(
  19. self,
  20. *,
  21. base_url: str,
  22. api_key: str,
  23. workspace: str,
  24. transport=None,
  25. timeout_seconds: float = 15,
  26. failure_threshold: int = 3,
  27. cooldown_seconds: float = 30,
  28. ) -> None:
  29. if not base_url or not api_key or not workspace:
  30. raise ValueError("LightRAG base URL, API key and workspace are required")
  31. self._base_url = base_url.rstrip("/")
  32. self._api_key = api_key
  33. self.workspace = workspace
  34. self._transport = transport or _RequestsTransport()
  35. self._timeout = timeout_seconds
  36. self._failure_threshold = failure_threshold
  37. self._cooldown = cooldown_seconds
  38. self._failures = 0
  39. self._opened_at: float | None = None
  40. def _request(
  41. self,
  42. method: str,
  43. path: str,
  44. *,
  45. json: dict[str, Any] | None = None,
  46. idempotency_key: str | None = None,
  47. ) -> dict[str, Any]:
  48. now = time.monotonic()
  49. if self._opened_at is not None:
  50. if now - self._opened_at < self._cooldown:
  51. raise LightRAGCircuitOpen("LightRAG circuit breaker is open")
  52. self._opened_at = None
  53. self._failures = 0
  54. headers = {"X-API-Key": self._api_key}
  55. if idempotency_key:
  56. headers["Idempotency-Key"] = idempotency_key
  57. try:
  58. response = self._transport.request(
  59. method=method,
  60. url=f"{self._base_url}{path}",
  61. json=json,
  62. headers=headers,
  63. timeout=self._timeout,
  64. )
  65. response.raise_for_status()
  66. payload = response.json()
  67. except Exception:
  68. self._failures += 1
  69. if self._failures >= self._failure_threshold:
  70. self._opened_at = time.monotonic()
  71. raise
  72. self._failures = 0
  73. return payload if isinstance(payload, dict) else {"response": payload}
  74. def health(self) -> dict[str, Any]:
  75. return self._request("GET", "/health")
  76. def insert(
  77. self,
  78. *,
  79. external_document_id: str,
  80. content: str,
  81. metadata: dict[str, Any],
  82. ) -> ProjectionReceipt:
  83. payload = self._request(
  84. "POST",
  85. "/documents/text",
  86. json={
  87. "text": f"[DATAOPS_SOURCE {external_document_id}]\n{content}",
  88. "file_source": external_document_id,
  89. "metadata": metadata,
  90. },
  91. idempotency_key=external_document_id,
  92. )
  93. track_id = payload.get("track_id") or payload.get("data", {}).get("track_id")
  94. if not track_id:
  95. raise RuntimeError("LightRAG insert response has no track_id")
  96. return ProjectionReceipt(track_id=str(track_id))
  97. def track_status(self, track_id: str) -> str:
  98. payload = self._request("GET", f"/documents/track_status/{track_id}")
  99. value = payload.get("status") or payload.get("data", {}).get("status")
  100. return str(value or "unknown")
  101. def delete(self, external_document_id: str) -> DeleteReceipt:
  102. payload = self._request(
  103. "DELETE",
  104. "/documents/delete_document",
  105. json={"doc_id": external_document_id},
  106. idempotency_key=f"delete:{external_document_id}",
  107. )
  108. return DeleteReceipt(verified=payload.get("verified") is True)
  109. def query_context(self, query: str, *, mode: str = "mix", limit: int = 20) -> str:
  110. if mode not in {"mix", "hybrid", "local", "global", "naive"}:
  111. raise ValueError("unsupported LightRAG query mode")
  112. payload = self._request(
  113. "POST",
  114. "/query",
  115. json={
  116. "query": query,
  117. "mode": mode,
  118. "only_need_context": True,
  119. "top_k": min(max(limit, 1), 100),
  120. },
  121. )
  122. value = payload.get("response", payload.get("data", ""))
  123. return str(value)