larkup 0.1.14__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.
larkup-0.1.14/PKG-INFO ADDED
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: larkup
3
+ Version: 0.1.14
4
+ Summary: Official Python SDK for the Larkup API
5
+ Keywords: rag,retrieval-augmented-generation,larkup,ai,sdk
6
+ Author: Abdelrahman
7
+ Author-email: Abdelrahman <abdelrahmanaboneda@gmail.com>
8
+ License-Expression: MIT
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Requires-Dist: httpx>=0.28.1
18
+ Requires-Dist: pydantic>=2.13.4
19
+ Requires-Python: >=3.10
20
+ Project-URL: Homepage, https://larkup.de
21
+ Project-URL: Documentation, https://larkup.de/docs
22
+ Project-URL: Repository, https://github.com/BuddyHere-AI/buddy-rag
23
+ Description-Content-Type: text/markdown
24
+
25
+ # Larkup Python SDK
26
+
27
+ The official Python client for the **Larkup** platform.
28
+
29
+ This SDK provides a simple, Pythonic interface to interact with your running Larkup servers. It offers both synchronous and asynchronous clients, built on top of `httpx` and `pydantic`.
30
+
31
+ ## Installation
32
+
33
+ Install the package via pip or uv:
34
+
35
+ ```bash
36
+ pip install larkup
37
+ # or
38
+ uv pip install larkup
39
+ ```
40
+
41
+ ## Quick Start
42
+
43
+ ### Synchronous Client
44
+
45
+ ```python
46
+ from larkup import LarkupClient
47
+
48
+ # Initialize the client
49
+ client = LarkupClient(base_url="http://localhost:4567")
50
+
51
+ # 1. Query the RAG Pipeline
52
+ response = client.query(
53
+ query="What is Larkup?",
54
+ top_k=5
55
+ )
56
+ for hit in response.hits:
57
+ print(hit.document.content)
58
+
59
+ # 2. Add Documents to the Pipeline
60
+ client.documents.add(
61
+ content="Larkup is building a new learning system for the AI Era.",
62
+ metadata={"source": "manual-entry"}
63
+ )
64
+
65
+ # 3. Trigger Indexing
66
+ client.index()
67
+ print("Documents indexed successfully.")
68
+ ```
69
+
70
+ ### Asynchronous Client
71
+
72
+ ```python
73
+ import asyncio
74
+ from larkup import AsyncLarkupClient
75
+
76
+ async def main():
77
+ client = AsyncLarkupClient(base_url="http://localhost:4567")
78
+
79
+ response = await client.query(
80
+ query="What is the future of education?",
81
+ top_k=3
82
+ )
83
+ print(response.hits)
84
+
85
+ asyncio.run(main())
86
+ ```
87
+
88
+ ## Features
89
+
90
+ - **Sync & Async Support**: Use `LarkupClient` or `AsyncLarkupClient`.
91
+ - **Querying**: Execute semantic searches against your vector store.
92
+ - **Data Ingestion**: Add documents and web pages programmatically.
93
+ - **Strong Typing**: Built with Pydantic for validation and editor autocompletion.
94
+
95
+ ## Documentation
96
+
97
+ For full API reference and advanced usage, visit the [Larkup Documentation](https://larkup.de/docs).
@@ -0,0 +1,73 @@
1
+ # Larkup Python SDK
2
+
3
+ The official Python client for the **Larkup** platform.
4
+
5
+ This SDK provides a simple, Pythonic interface to interact with your running Larkup servers. It offers both synchronous and asynchronous clients, built on top of `httpx` and `pydantic`.
6
+
7
+ ## Installation
8
+
9
+ Install the package via pip or uv:
10
+
11
+ ```bash
12
+ pip install larkup
13
+ # or
14
+ uv pip install larkup
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ### Synchronous Client
20
+
21
+ ```python
22
+ from larkup import LarkupClient
23
+
24
+ # Initialize the client
25
+ client = LarkupClient(base_url="http://localhost:4567")
26
+
27
+ # 1. Query the RAG Pipeline
28
+ response = client.query(
29
+ query="What is Larkup?",
30
+ top_k=5
31
+ )
32
+ for hit in response.hits:
33
+ print(hit.document.content)
34
+
35
+ # 2. Add Documents to the Pipeline
36
+ client.documents.add(
37
+ content="Larkup is building a new learning system for the AI Era.",
38
+ metadata={"source": "manual-entry"}
39
+ )
40
+
41
+ # 3. Trigger Indexing
42
+ client.index()
43
+ print("Documents indexed successfully.")
44
+ ```
45
+
46
+ ### Asynchronous Client
47
+
48
+ ```python
49
+ import asyncio
50
+ from larkup import AsyncLarkupClient
51
+
52
+ async def main():
53
+ client = AsyncLarkupClient(base_url="http://localhost:4567")
54
+
55
+ response = await client.query(
56
+ query="What is the future of education?",
57
+ top_k=3
58
+ )
59
+ print(response.hits)
60
+
61
+ asyncio.run(main())
62
+ ```
63
+
64
+ ## Features
65
+
66
+ - **Sync & Async Support**: Use `LarkupClient` or `AsyncLarkupClient`.
67
+ - **Querying**: Execute semantic searches against your vector store.
68
+ - **Data Ingestion**: Add documents and web pages programmatically.
69
+ - **Strong Typing**: Built with Pydantic for validation and editor autocompletion.
70
+
71
+ ## Documentation
72
+
73
+ For full API reference and advanced usage, visit the [Larkup Documentation](https://larkup.de/docs).
@@ -0,0 +1,41 @@
1
+ [project]
2
+ name = "larkup"
3
+ version = "0.1.14"
4
+ description = "Official Python SDK for the Larkup API"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [
8
+ { name = "Abdelrahman", email = "abdelrahmanaboneda@gmail.com" }
9
+ ]
10
+ requires-python = ">=3.10"
11
+ keywords = ["rag", "retrieval-augmented-generation", "larkup", "ai", "sdk"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Intended Audience :: Developers",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
21
+ ]
22
+ dependencies = [
23
+ "httpx>=0.28.1",
24
+ "pydantic>=2.13.4",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://larkup.de"
29
+ Documentation = "https://larkup.de/docs"
30
+ Repository = "https://github.com/BuddyHere-AI/buddy-rag"
31
+
32
+ [build-system]
33
+ requires = ["uv_build>=0.11.14,<0.12.0"]
34
+ build-backend = "uv_build"
35
+
36
+ [dependency-groups]
37
+ dev = [
38
+ "pytest>=9.1.1",
39
+ "pytest-asyncio>=1.4.0",
40
+ "respx>=0.23.1",
41
+ ]
@@ -0,0 +1,27 @@
1
+ from .client import LarkupClient, AsyncLarkupClient, LarkupError
2
+ from .types import (
3
+ Document,
4
+ QueryRequest,
5
+ QueryHit,
6
+ QueryResponse,
7
+ PaginatedDocuments,
8
+ ScrapeRequest,
9
+ ScrapeResponse,
10
+ HealthResponse,
11
+ LarkupClientOptions,
12
+ )
13
+
14
+ __all__ = [
15
+ "LarkupClient",
16
+ "AsyncLarkupClient",
17
+ "LarkupError",
18
+ "Document",
19
+ "QueryRequest",
20
+ "QueryHit",
21
+ "QueryResponse",
22
+ "PaginatedDocuments",
23
+ "ScrapeRequest",
24
+ "ScrapeResponse",
25
+ "HealthResponse",
26
+ "LarkupClientOptions",
27
+ ]
@@ -0,0 +1,214 @@
1
+ import os
2
+ import httpx
3
+ from typing import Optional, Union, Dict, Any
4
+ from .types import (
5
+ Document,
6
+ PaginatedDocuments,
7
+ QueryRequest,
8
+ QueryResponse,
9
+ ScrapeResponse,
10
+ HealthResponse,
11
+ LarkupClientOptions,
12
+ )
13
+
14
+ class LarkupError(Exception):
15
+ def __init__(self, message: str, status_code: Optional[int] = None):
16
+ super().__init__(message)
17
+ self.status_code = status_code
18
+
19
+ class LarkupClient:
20
+ """Synchronous client for Larkup API."""
21
+
22
+ def __init__(self, options: Optional[LarkupClientOptions] = None):
23
+ options = options or LarkupClientOptions()
24
+
25
+ base_url = options.base_url or os.getenv("LARKUP_API_URL", "http://localhost:8080")
26
+ self.base_url = base_url.rstrip("/")
27
+ self.api_key = options.api_key or os.getenv("LARKUP_API_KEY")
28
+ self._client = httpx.Client(base_url=self.base_url)
29
+
30
+ def _get_headers(self) -> Dict[str, str]:
31
+ headers = {}
32
+ if self.api_key:
33
+ headers["Authorization"] = f"Bearer {self.api_key}"
34
+ return headers
35
+
36
+ def _handle_error(self, response: httpx.Response):
37
+ if not response.is_success:
38
+ error_msg = response.reason_phrase
39
+ try:
40
+ err_body = response.json()
41
+ if isinstance(err_body, dict) and "error" in err_body:
42
+ error_msg = err_body["error"]
43
+ except Exception:
44
+ pass
45
+ raise LarkupError(f"Larkup API Error ({response.status_code}): {error_msg}", response.status_code)
46
+
47
+ def _request(self, method: str, path: str, **kwargs) -> Any:
48
+ headers = self._get_headers()
49
+ if "headers" in kwargs:
50
+ headers.update(kwargs.pop("headers"))
51
+
52
+ response = self._client.request(method, path, headers=headers, **kwargs)
53
+ self._handle_error(response)
54
+ return response.json()
55
+
56
+ def health(self) -> HealthResponse:
57
+ """Health check"""
58
+ data = self._request("GET", "/health")
59
+ return HealthResponse(**data)
60
+
61
+ def query(self, request: Union[QueryRequest, str], top_k: Optional[int] = None) -> QueryResponse:
62
+ """Query the RAG knowledge base"""
63
+ if isinstance(request, str):
64
+ request_data = QueryRequest(query=request, topK=top_k)
65
+ else:
66
+ request_data = request
67
+
68
+ data = self._request("POST", "/query", json=request_data.model_dump(exclude_none=True))
69
+ return QueryResponse(**data)
70
+
71
+ def list_documents(self, page: int = 1, limit: int = 20) -> PaginatedDocuments:
72
+ """List documents with pagination"""
73
+ data = self._request("GET", "/documents", params={"page": page, "limit": limit})
74
+ return PaginatedDocuments(**data)
75
+
76
+ def get_document(self, id: str) -> Document:
77
+ """Get a specific document by ID"""
78
+ data = self._request("GET", f"/documents/{id}")
79
+ return Document(**data)
80
+
81
+ def add_document(self, document: Document) -> Dict[str, Any]:
82
+ """Add a new document to the vector store"""
83
+ # We allow 'id' in Document to be optionally ignored by the server,
84
+ # but to mirror JS we just send it as dict.
85
+ payload = document.model_dump(exclude_none=True)
86
+ if "id" in payload and not payload["id"]:
87
+ del payload["id"]
88
+
89
+ data = self._request("POST", "/documents", json=payload)
90
+ return data
91
+
92
+ def update_document(self, id: str, document: Document) -> Dict[str, Any]:
93
+ """Update an existing document"""
94
+ payload = document.model_dump(exclude_none=True)
95
+ data = self._request("PUT", f"/documents/{id}", json=payload)
96
+ return data
97
+
98
+ def delete_document(self, id: str) -> Dict[str, Any]:
99
+ """Delete a document"""
100
+ data = self._request("DELETE", f"/documents/{id}")
101
+ return data
102
+
103
+ def scrape(self, url: str) -> ScrapeResponse:
104
+ """Scrape a URL and add it to the corpus"""
105
+ data = self._request("POST", "/scrape", json={"url": url})
106
+ return ScrapeResponse(**data)
107
+
108
+ def close(self):
109
+ """Close the underlying HTTP client"""
110
+ self._client.close()
111
+
112
+ def __enter__(self):
113
+ return self
114
+
115
+ def __exit__(self, exc_type, exc_val, exc_tb):
116
+ self.close()
117
+
118
+
119
+ class AsyncLarkupClient:
120
+ """Asynchronous client for Larkup API."""
121
+
122
+ def __init__(self, options: Optional[LarkupClientOptions] = None):
123
+ options = options or LarkupClientOptions()
124
+
125
+ base_url = options.base_url or os.getenv("LARKUP_API_URL", "http://localhost:8080")
126
+ self.base_url = base_url.rstrip("/")
127
+ self.api_key = options.api_key or os.getenv("LARKUP_API_KEY")
128
+ self._client = httpx.AsyncClient(base_url=self.base_url)
129
+
130
+ def _get_headers(self) -> Dict[str, str]:
131
+ headers = {}
132
+ if self.api_key:
133
+ headers["Authorization"] = f"Bearer {self.api_key}"
134
+ return headers
135
+
136
+ def _handle_error(self, response: httpx.Response):
137
+ if not response.is_success:
138
+ error_msg = response.reason_phrase
139
+ try:
140
+ err_body = response.json()
141
+ if isinstance(err_body, dict) and "error" in err_body:
142
+ error_msg = err_body["error"]
143
+ except Exception:
144
+ pass
145
+ raise LarkupError(f"Larkup API Error ({response.status_code}): {error_msg}", response.status_code)
146
+
147
+ async def _request(self, method: str, path: str, **kwargs) -> Any:
148
+ headers = self._get_headers()
149
+ if "headers" in kwargs:
150
+ headers.update(kwargs.pop("headers"))
151
+
152
+ response = await self._client.request(method, path, headers=headers, **kwargs)
153
+ self._handle_error(response)
154
+ return response.json()
155
+
156
+ async def health(self) -> HealthResponse:
157
+ """Health check"""
158
+ data = await self._request("GET", "/health")
159
+ return HealthResponse(**data)
160
+
161
+ async def query(self, request: Union[QueryRequest, str], top_k: Optional[int] = None) -> QueryResponse:
162
+ """Query the RAG knowledge base"""
163
+ if isinstance(request, str):
164
+ request_data = QueryRequest(query=request, topK=top_k)
165
+ else:
166
+ request_data = request
167
+
168
+ data = await self._request("POST", "/query", json=request_data.model_dump(exclude_none=True))
169
+ return QueryResponse(**data)
170
+
171
+ async def list_documents(self, page: int = 1, limit: int = 20) -> PaginatedDocuments:
172
+ """List documents with pagination"""
173
+ data = await self._request("GET", "/documents", params={"page": page, "limit": limit})
174
+ return PaginatedDocuments(**data)
175
+
176
+ async def get_document(self, id: str) -> Document:
177
+ """Get a specific document by ID"""
178
+ data = await self._request("GET", f"/documents/{id}")
179
+ return Document(**data)
180
+
181
+ async def add_document(self, document: Document) -> Dict[str, Any]:
182
+ """Add a new document to the vector store"""
183
+ payload = document.model_dump(exclude_none=True)
184
+ if "id" in payload and not payload["id"]:
185
+ del payload["id"]
186
+
187
+ data = await self._request("POST", "/documents", json=payload)
188
+ return data
189
+
190
+ async def update_document(self, id: str, document: Document) -> Dict[str, Any]:
191
+ """Update an existing document"""
192
+ payload = document.model_dump(exclude_none=True)
193
+ data = await self._request("PUT", f"/documents/{id}", json=payload)
194
+ return data
195
+
196
+ async def delete_document(self, id: str) -> Dict[str, Any]:
197
+ """Delete a document"""
198
+ data = await self._request("DELETE", f"/documents/{id}")
199
+ return data
200
+
201
+ async def scrape(self, url: str) -> ScrapeResponse:
202
+ """Scrape a URL and add it to the corpus"""
203
+ data = await self._request("POST", "/scrape", json={"url": url})
204
+ return ScrapeResponse(**data)
205
+
206
+ async def close(self):
207
+ """Close the underlying HTTP client"""
208
+ await self._client.aclose()
209
+
210
+ async def __aenter__(self):
211
+ return self
212
+
213
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
214
+ await self.close()
File without changes
@@ -0,0 +1,48 @@
1
+ from typing import List, Optional
2
+ from pydantic import BaseModel, Field
3
+
4
+ class Document(BaseModel):
5
+ id: str
6
+ text: str
7
+ title: Optional[str] = None
8
+ url: Optional[str] = None
9
+ documentId: Optional[str] = None
10
+
11
+ class QueryRequest(BaseModel):
12
+ query: str
13
+ topK: Optional[int] = None
14
+
15
+ class QueryHit(BaseModel):
16
+ id: str
17
+ score: float
18
+ text: str
19
+ title: str
20
+ url: Optional[str] = None
21
+ documentId: str
22
+
23
+ class QueryResponse(BaseModel):
24
+ query: str
25
+ hits: List[QueryHit]
26
+
27
+ class PaginatedDocuments(BaseModel):
28
+ documents: List[Document]
29
+ total: int
30
+ page: int
31
+ limit: int
32
+ totalPages: int
33
+
34
+ class ScrapeRequest(BaseModel):
35
+ url: str
36
+
37
+ class ScrapeResponse(BaseModel):
38
+ success: bool
39
+ documentId: Optional[str] = None
40
+ error: Optional[str] = None
41
+
42
+ class HealthResponse(BaseModel):
43
+ ok: bool
44
+ service: Optional[str] = None
45
+
46
+ class LarkupClientOptions(BaseModel):
47
+ base_url: Optional[str] = None
48
+ api_key: Optional[str] = None