rag-plug 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: rag-plug
3
+ Version: 0.1.0
4
+ Summary: RAG client
5
+ Author: George K
6
+ Author-email: george@dormint.io
7
+ Requires-Python: >=3.10,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Requires-Dist: httpx (>=0.28.1,<0.29.0)
15
+ Requires-Dist: openai-agents (>=0.9.2,<0.10.0)
16
+ Description-Content-Type: text/markdown
17
+
18
+ # rag-plug
19
+
20
+ Python-клиент для backend RAGPlug API.
21
+ Поддерживает добавление памяти, семантический поиск и удаление записей по ID (sync/async).
22
+
23
+ ## Возможности
24
+ - `RagPlug` с типизированными моделями ответов.
25
+ - Аутентификация через `X-API-Key`.
26
+ - Операции в рамках конкретной памяти (`memory_name`).
27
+ - Готовые CLI-скрипты для быстрого add/search.
28
+
29
+ ## Требования
30
+ - Python `3.10+`
31
+ - [Poetry](https://python-poetry.org/) (рекомендуется)
32
+
33
+ ## Установка
34
+ ```bash
35
+ poetry install
36
+ ```
37
+
38
+ Или через pip (локальная разработка):
39
+ ```bash
40
+ pip install -e .
41
+ ```
42
+
43
+ ## Конфигурация
44
+ Создайте `.env` в корне проекта:
45
+
46
+ ```env
47
+ API_KEY=your_api_key
48
+ MEMORY_NAME=your_memory_name
49
+ ```
50
+
51
+ `.env` добавлен в `.gitignore` и не должен попадать в репозиторий.
52
+
53
+ ## Использование в Python
54
+ ```python
55
+ from ragplug import RagPlug
56
+
57
+ client = RagPlug(api_key="YOUR_API_KEY", default_memory_name="main")
58
+
59
+ item = client.add("Paris is the capital of France", metadata={"source": "docs"})
60
+ result = client.search("capital of France", top_k=3)
61
+ client.delete(item.id)
62
+ ```
63
+
64
+ Async-пример:
65
+ ```python
66
+ import asyncio
67
+ from ragplug import RagPlug
68
+
69
+ async def main():
70
+ client = RagPlug(api_key="YOUR_API_KEY", default_memory_name="main")
71
+ await client.aadd("Async memory text")
72
+ res = await client.asearch("Async memory")
73
+ print(res.results)
74
+
75
+ asyncio.run(main())
76
+ ```
77
+
78
+ ## CLI-скрипты
79
+ - `scripts/add_memory.py`
80
+ - `scripts/search.py`
81
+
82
+ Пример запуска:
83
+ ```bash
84
+ set -a; source .env; set +a
85
+
86
+ python scripts/add_memory.py \
87
+ --api-key "$API_KEY" \
88
+ --memory-name "$MEMORY_NAME" \
89
+ --text "Smoke test memory" \
90
+ --metadata '{"source":"scripts"}'
91
+
92
+ python scripts/search.py \
93
+ --api-key "$API_KEY" \
94
+ --memory-name "$MEMORY_NAME" \
95
+ --query "Smoke test" \
96
+ --top-k 5
97
+ ```
98
+
99
+ ## Команды разработки
100
+ ```bash
101
+ poetry check # проверка метаданных пакета
102
+ poetry build # сборка sdist + wheel
103
+ python -m compileall ragplug scripts
104
+ ```
105
+
106
+ ## Релиз
107
+ CI публикует пакет в PyPI по тегам `v*`.
108
+ Тег должен совпадать с версией в `pyproject.toml`.
109
+
110
+ Пример:
111
+ ```bash
112
+ poetry version patch
113
+ git tag v$(poetry version -s)
114
+ git push origin --tags
115
+ ```
116
+
117
+ ## Структура проекта
118
+ - `ragplug/client.py`: публичный фасад `RagPlug`.
119
+ - `ragplug/types.py`: модели ответов и `RagPlugError`.
120
+ - `ragplug/_api.py`, `_transport.py`, `_error_handler.py`, `_validation.py`, `_response_parser.py`: внутренние слои клиента.
121
+ - `scripts/`: CLI-утилиты.
122
+
@@ -0,0 +1,104 @@
1
+ # rag-plug
2
+
3
+ Python-клиент для backend RAGPlug API.
4
+ Поддерживает добавление памяти, семантический поиск и удаление записей по ID (sync/async).
5
+
6
+ ## Возможности
7
+ - `RagPlug` с типизированными моделями ответов.
8
+ - Аутентификация через `X-API-Key`.
9
+ - Операции в рамках конкретной памяти (`memory_name`).
10
+ - Готовые CLI-скрипты для быстрого add/search.
11
+
12
+ ## Требования
13
+ - Python `3.10+`
14
+ - [Poetry](https://python-poetry.org/) (рекомендуется)
15
+
16
+ ## Установка
17
+ ```bash
18
+ poetry install
19
+ ```
20
+
21
+ Или через pip (локальная разработка):
22
+ ```bash
23
+ pip install -e .
24
+ ```
25
+
26
+ ## Конфигурация
27
+ Создайте `.env` в корне проекта:
28
+
29
+ ```env
30
+ API_KEY=your_api_key
31
+ MEMORY_NAME=your_memory_name
32
+ ```
33
+
34
+ `.env` добавлен в `.gitignore` и не должен попадать в репозиторий.
35
+
36
+ ## Использование в Python
37
+ ```python
38
+ from ragplug import RagPlug
39
+
40
+ client = RagPlug(api_key="YOUR_API_KEY", default_memory_name="main")
41
+
42
+ item = client.add("Paris is the capital of France", metadata={"source": "docs"})
43
+ result = client.search("capital of France", top_k=3)
44
+ client.delete(item.id)
45
+ ```
46
+
47
+ Async-пример:
48
+ ```python
49
+ import asyncio
50
+ from ragplug import RagPlug
51
+
52
+ async def main():
53
+ client = RagPlug(api_key="YOUR_API_KEY", default_memory_name="main")
54
+ await client.aadd("Async memory text")
55
+ res = await client.asearch("Async memory")
56
+ print(res.results)
57
+
58
+ asyncio.run(main())
59
+ ```
60
+
61
+ ## CLI-скрипты
62
+ - `scripts/add_memory.py`
63
+ - `scripts/search.py`
64
+
65
+ Пример запуска:
66
+ ```bash
67
+ set -a; source .env; set +a
68
+
69
+ python scripts/add_memory.py \
70
+ --api-key "$API_KEY" \
71
+ --memory-name "$MEMORY_NAME" \
72
+ --text "Smoke test memory" \
73
+ --metadata '{"source":"scripts"}'
74
+
75
+ python scripts/search.py \
76
+ --api-key "$API_KEY" \
77
+ --memory-name "$MEMORY_NAME" \
78
+ --query "Smoke test" \
79
+ --top-k 5
80
+ ```
81
+
82
+ ## Команды разработки
83
+ ```bash
84
+ poetry check # проверка метаданных пакета
85
+ poetry build # сборка sdist + wheel
86
+ python -m compileall ragplug scripts
87
+ ```
88
+
89
+ ## Релиз
90
+ CI публикует пакет в PyPI по тегам `v*`.
91
+ Тег должен совпадать с версией в `pyproject.toml`.
92
+
93
+ Пример:
94
+ ```bash
95
+ poetry version patch
96
+ git tag v$(poetry version -s)
97
+ git push origin --tags
98
+ ```
99
+
100
+ ## Структура проекта
101
+ - `ragplug/client.py`: публичный фасад `RagPlug`.
102
+ - `ragplug/types.py`: модели ответов и `RagPlugError`.
103
+ - `ragplug/_api.py`, `_transport.py`, `_error_handler.py`, `_validation.py`, `_response_parser.py`: внутренние слои клиента.
104
+ - `scripts/`: CLI-утилиты.
@@ -0,0 +1,17 @@
1
+ [tool.poetry]
2
+ name = "rag-plug"
3
+ version = "0.1.0"
4
+ description = "RAG client"
5
+ authors = ["George K <george@dormint.io>"]
6
+ readme = "README.md"
7
+ packages = [{ include = "ragplug" }]
8
+
9
+ [tool.poetry.dependencies]
10
+ python = "^3.10"
11
+ openai-agents = "^0.9.2"
12
+ httpx = "^0.28.1"
13
+
14
+
15
+ [build-system]
16
+ requires = ["poetry-core"]
17
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,17 @@
1
+ from .client import RagPlug
2
+ from .types import (
3
+ MemoryDeleteResult,
4
+ MemoryItem,
5
+ RagPlugError,
6
+ SearchResponse,
7
+ SearchResult,
8
+ )
9
+
10
+ __all__ = [
11
+ "RagPlug",
12
+ "RagPlugError",
13
+ "MemoryItem",
14
+ "MemoryDeleteResult",
15
+ "SearchResult",
16
+ "SearchResponse",
17
+ ]
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict
4
+ from urllib.parse import quote
5
+
6
+ from ragplug._transport import _HttpTransport
7
+
8
+
9
+ class _RagPlugApi:
10
+ def __init__(self, transport: _HttpTransport) -> None:
11
+ self._transport = transport
12
+
13
+ @staticmethod
14
+ def _segment(value: str) -> str:
15
+ return quote(value, safe="")
16
+
17
+ def version(self) -> Dict[str, Any]:
18
+ return self._transport.request_json("GET", "/version")
19
+
20
+ async def aversion(self) -> Dict[str, Any]:
21
+ return await self._transport.arequest_json("GET", "/version")
22
+
23
+ def add_memory(self, memory_name: str, payload: Dict[str, Any]) -> Dict[str, Any]:
24
+ path = f"/memory/{self._segment(memory_name)}"
25
+ return self._transport.request_json("POST", path, payload=payload)
26
+
27
+ async def aadd_memory(self, memory_name: str, payload: Dict[str, Any]) -> Dict[str, Any]:
28
+ path = f"/memory/{self._segment(memory_name)}"
29
+ return await self._transport.arequest_json("POST", path, payload=payload)
30
+
31
+ def delete_memory(self, memory_name: str, item_id: str) -> Dict[str, Any]:
32
+ path = f"/memory/{self._segment(memory_name)}/{self._segment(item_id)}"
33
+ return self._transport.request_json("DELETE", path)
34
+
35
+ async def adelete_memory(self, memory_name: str, item_id: str) -> Dict[str, Any]:
36
+ path = f"/memory/{self._segment(memory_name)}/{self._segment(item_id)}"
37
+ return await self._transport.arequest_json("DELETE", path)
38
+
39
+ def search_memory(self, memory_name: str, payload: Dict[str, Any]) -> Dict[str, Any]:
40
+ path = f"/search/{self._segment(memory_name)}"
41
+ return self._transport.request_json("POST", path, payload=payload)
42
+
43
+ async def asearch_memory(self, memory_name: str, payload: Dict[str, Any]) -> Dict[str, Any]:
44
+ path = f"/search/{self._segment(memory_name)}"
45
+ return await self._transport.arequest_json("POST", path, payload=payload)
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict
4
+
5
+ import httpx
6
+
7
+ from ragplug.types import RagPlugError
8
+
9
+
10
+ class _ErrorHandler:
11
+ @staticmethod
12
+ def raise_for_http_error(response: httpx.Response) -> None:
13
+ if response.is_success:
14
+ return
15
+
16
+ detail = None
17
+ try:
18
+ payload = response.json()
19
+ if isinstance(payload, dict):
20
+ detail = payload.get("detail")
21
+ except ValueError:
22
+ detail = None
23
+
24
+ message = str(detail) if detail else response.text.strip() or "Request failed"
25
+ raise RagPlugError(message=message, status_code=response.status_code)
26
+
27
+ @staticmethod
28
+ def raise_network_error(error: httpx.RequestError) -> None:
29
+ raise RagPlugError(f"Network error: {error}") from error
30
+
31
+ @staticmethod
32
+ def parse_json_dict(response: httpx.Response) -> Dict[str, Any]:
33
+ try:
34
+ data = response.json()
35
+ except ValueError as error:
36
+ raise RagPlugError(
37
+ "Invalid JSON response",
38
+ status_code=response.status_code,
39
+ ) from error
40
+
41
+ if not isinstance(data, dict):
42
+ raise RagPlugError(
43
+ "Unexpected response format",
44
+ status_code=response.status_code,
45
+ )
46
+ return data
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List
4
+
5
+ from ragplug.types import (
6
+ MemoryDeleteResult,
7
+ MemoryItem,
8
+ RagPlugError,
9
+ SearchResponse,
10
+ SearchResult,
11
+ )
12
+
13
+
14
+ class _ResponseParser:
15
+ @staticmethod
16
+ def version(data: Dict[str, Any]) -> str:
17
+ version_value = data.get("version")
18
+ if version_value is None:
19
+ raise RagPlugError("Unexpected /version response format")
20
+ return str(version_value)
21
+
22
+ @staticmethod
23
+ def memory_item(data: Dict[str, Any]) -> MemoryItem:
24
+ return MemoryItem(
25
+ id=str(data.get("id", "")),
26
+ text=str(data.get("text", "")),
27
+ metadata=data.get("metadata") if isinstance(data.get("metadata"), dict) else {},
28
+ )
29
+
30
+ @staticmethod
31
+ def memory_delete(data: Dict[str, Any], item_id: str) -> MemoryDeleteResult:
32
+ return MemoryDeleteResult(
33
+ id=str(data.get("id", item_id)),
34
+ deleted=bool(data.get("deleted", False)),
35
+ )
36
+
37
+ @staticmethod
38
+ def search(data: Dict[str, Any]) -> SearchResponse:
39
+ raw_results = data.get("results")
40
+ results: List[SearchResult] = []
41
+
42
+ if isinstance(raw_results, list):
43
+ for row in raw_results:
44
+ if not isinstance(row, dict):
45
+ continue
46
+ results.append(
47
+ SearchResult(
48
+ id=str(row.get("id", "")),
49
+ text=str(row.get("text", "")),
50
+ metadata=row.get("metadata") if isinstance(row.get("metadata"), dict) else {},
51
+ score=float(row.get("score", 0.0) or 0.0),
52
+ )
53
+ )
54
+
55
+ return SearchResponse(query=str(data.get("query", "")), results=results)
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ import httpx
6
+
7
+ from ragplug._error_handler import _ErrorHandler
8
+
9
+
10
+ class _HttpTransport:
11
+ def __init__(self, endpoint_url: str, api_key: str, timeout: float) -> None:
12
+ self.endpoint_url = endpoint_url.rstrip("/")
13
+ self.api_key = api_key
14
+ self.timeout = timeout
15
+
16
+ @property
17
+ def headers(self) -> Dict[str, str]:
18
+ return {
19
+ "X-API-Key": self.api_key,
20
+ "Content-Type": "application/json",
21
+ "Accept": "application/json",
22
+ }
23
+
24
+ def _url(self, path: str) -> str:
25
+ return f"{self.endpoint_url}{path}"
26
+
27
+ def request_json(
28
+ self,
29
+ method: str,
30
+ path: str,
31
+ payload: Optional[Dict[str, Any]] = None,
32
+ ) -> Dict[str, Any]:
33
+ try:
34
+ with httpx.Client(timeout=self.timeout, headers=self.headers) as client:
35
+ response = client.request(method, self._url(path), json=payload)
36
+ except httpx.RequestError as error:
37
+ _ErrorHandler.raise_network_error(error)
38
+
39
+ _ErrorHandler.raise_for_http_error(response)
40
+ return _ErrorHandler.parse_json_dict(response)
41
+
42
+ async def arequest_json(
43
+ self,
44
+ method: str,
45
+ path: str,
46
+ payload: Optional[Dict[str, Any]] = None,
47
+ ) -> Dict[str, Any]:
48
+ try:
49
+ async with httpx.AsyncClient(timeout=self.timeout, headers=self.headers) as client:
50
+ response = await client.request(method, self._url(path), json=payload)
51
+ except httpx.RequestError as error:
52
+ _ErrorHandler.raise_network_error(error)
53
+
54
+ _ErrorHandler.raise_for_http_error(response)
55
+ return _ErrorHandler.parse_json_dict(response)
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+
6
+ class _Validator:
7
+ @staticmethod
8
+ def require_non_empty(value: str, name: str) -> str:
9
+ if not value or not value.strip():
10
+ raise ValueError(f"{name} must be a non-empty string")
11
+ return value
12
+
13
+ @staticmethod
14
+ def validate_top_k(top_k: int) -> int:
15
+ if top_k < 1 or top_k > 50:
16
+ raise ValueError("top_k must be between 1 and 50")
17
+ return top_k
18
+
19
+ @staticmethod
20
+ def resolve_memory_name(memory_name: Optional[str], default_memory_name: Optional[str]) -> str:
21
+ name = memory_name or default_memory_name
22
+ if not name or not name.strip():
23
+ raise ValueError("memory_name is required (or set default_memory_name in RagPlug)")
24
+ return name
@@ -0,0 +1,121 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from ragplug._api import _RagPlugApi
6
+ from ragplug._response_parser import _ResponseParser
7
+ from ragplug._transport import _HttpTransport
8
+ from ragplug._validation import _Validator
9
+ from ragplug.types import MemoryDeleteResult, MemoryItem, SearchResponse
10
+
11
+
12
+ class RagPlug:
13
+ endpoint_url = "https://api-ragplug.dormint.io"
14
+
15
+ def __init__(
16
+ self,
17
+ api_key: str,
18
+ endpoint_url: Optional[str] = None,
19
+ default_memory_name: Optional[str] = None,
20
+ timeout: float = 30.0,
21
+ ) -> None:
22
+ self.api_key = api_key
23
+ self.endpoint_url = (endpoint_url or self.endpoint_url).rstrip("/")
24
+ self.default_memory_name = default_memory_name
25
+ self.timeout = timeout
26
+
27
+ self._transport = _HttpTransport(
28
+ endpoint_url=self.endpoint_url,
29
+ api_key=self.api_key,
30
+ timeout=self.timeout,
31
+ )
32
+ self._api = _RagPlugApi(self._transport)
33
+
34
+ def _memory_name(self, memory_name: Optional[str]) -> str:
35
+ return _Validator.resolve_memory_name(memory_name, self.default_memory_name)
36
+
37
+ def version(self) -> str:
38
+ data = self._api.version()
39
+ return _ResponseParser.version(data)
40
+
41
+ async def aversion(self) -> str:
42
+ data = await self._api.aversion()
43
+ return _ResponseParser.version(data)
44
+
45
+ def add(
46
+ self,
47
+ text: str,
48
+ memory_name: Optional[str] = None,
49
+ metadata: Optional[Dict[str, Any]] = None,
50
+ item_id: Optional[str] = None,
51
+ ) -> MemoryItem:
52
+ _Validator.require_non_empty(text, "text")
53
+ resolved_memory_name = self._memory_name(memory_name)
54
+
55
+ payload: Dict[str, Any] = {
56
+ "text": text,
57
+ "metadata": metadata or {},
58
+ "id": item_id,
59
+ }
60
+ data = self._api.add_memory(resolved_memory_name, payload)
61
+ return _ResponseParser.memory_item(data)
62
+
63
+ async def aadd(
64
+ self,
65
+ text: str,
66
+ memory_name: Optional[str] = None,
67
+ metadata: Optional[Dict[str, Any]] = None,
68
+ item_id: Optional[str] = None,
69
+ ) -> MemoryItem:
70
+ _Validator.require_non_empty(text, "text")
71
+ resolved_memory_name = self._memory_name(memory_name)
72
+
73
+ payload: Dict[str, Any] = {
74
+ "text": text,
75
+ "metadata": metadata or {},
76
+ "id": item_id,
77
+ }
78
+ data = await self._api.aadd_memory(resolved_memory_name, payload)
79
+ return _ResponseParser.memory_item(data)
80
+
81
+ def delete(self, item_id: str, memory_name: Optional[str] = None) -> MemoryDeleteResult:
82
+ _Validator.require_non_empty(item_id, "item_id")
83
+ resolved_memory_name = self._memory_name(memory_name)
84
+
85
+ data = self._api.delete_memory(resolved_memory_name, item_id)
86
+ return _ResponseParser.memory_delete(data, item_id)
87
+
88
+ async def adelete(self, item_id: str, memory_name: Optional[str] = None) -> MemoryDeleteResult:
89
+ _Validator.require_non_empty(item_id, "item_id")
90
+ resolved_memory_name = self._memory_name(memory_name)
91
+
92
+ data = await self._api.adelete_memory(resolved_memory_name, item_id)
93
+ return _ResponseParser.memory_delete(data, item_id)
94
+
95
+ def search(
96
+ self,
97
+ query: str,
98
+ top_k: int = 5,
99
+ memory_name: Optional[str] = None,
100
+ ) -> SearchResponse:
101
+ _Validator.require_non_empty(query, "query")
102
+ _Validator.validate_top_k(top_k)
103
+ resolved_memory_name = self._memory_name(memory_name)
104
+
105
+ payload = {"query": query, "top_k": top_k}
106
+ data = self._api.search_memory(resolved_memory_name, payload)
107
+ return _ResponseParser.search(data)
108
+
109
+ async def asearch(
110
+ self,
111
+ query: str,
112
+ top_k: int = 5,
113
+ memory_name: Optional[str] = None,
114
+ ) -> SearchResponse:
115
+ _Validator.require_non_empty(query, "query")
116
+ _Validator.validate_top_k(top_k)
117
+ resolved_memory_name = self._memory_name(memory_name)
118
+
119
+ payload = {"query": query, "top_k": top_k}
120
+ data = await self._api.asearch_memory(resolved_memory_name, payload)
121
+ return _ResponseParser.search(data)
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, Dict, List, Optional
5
+
6
+
7
+ @dataclass(slots=True)
8
+ class MemoryItem:
9
+ id: str
10
+ text: str
11
+ metadata: Dict[str, Any] = field(default_factory=dict)
12
+
13
+
14
+ @dataclass(slots=True)
15
+ class MemoryDeleteResult:
16
+ id: str
17
+ deleted: bool
18
+
19
+
20
+ @dataclass(slots=True)
21
+ class SearchResult:
22
+ id: str
23
+ text: str
24
+ metadata: Dict[str, Any] = field(default_factory=dict)
25
+ score: float = 0.0
26
+
27
+
28
+ @dataclass(slots=True)
29
+ class SearchResponse:
30
+ query: str
31
+ results: List[SearchResult] = field(default_factory=list)
32
+
33
+
34
+ class RagPlugError(RuntimeError):
35
+ def __init__(self, message: str, status_code: Optional[int] = None) -> None:
36
+ super().__init__(message)
37
+ self.status_code = status_code