larkup 0.1.14__py3-none-any.whl
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/__init__.py +27 -0
- larkup/client.py +214 -0
- larkup/py.typed +0 -0
- larkup/types.py +48 -0
- larkup-0.1.14.dist-info/METADATA +97 -0
- larkup-0.1.14.dist-info/RECORD +7 -0
- larkup-0.1.14.dist-info/WHEEL +4 -0
larkup/__init__.py
ADDED
|
@@ -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
|
+
]
|
larkup/client.py
ADDED
|
@@ -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()
|
larkup/py.typed
ADDED
|
File without changes
|
larkup/types.py
ADDED
|
@@ -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
|
|
@@ -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,7 @@
|
|
|
1
|
+
larkup/__init__.py,sha256=jldAJEbpxRBSHgO-UTqh-jlIALUphmsB2V28E91M5eI,531
|
|
2
|
+
larkup/client.py,sha256=R0WTJMI5irqc5s0wBOBU6NYf_gJshj_Hb31aixNwNo4,8167
|
|
3
|
+
larkup/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
larkup/types.py,sha256=WWLFWzWjORZlockwMLvFzUKuv01IZ9f79RYPeNp0iIo,1000
|
|
5
|
+
larkup-0.1.14.dist-info/WHEEL,sha256=fWriCkzqm-pffF5af4gJC9iI5FMFaJTuN9UxxxzOmdY,81
|
|
6
|
+
larkup-0.1.14.dist-info/METADATA,sha256=Op4LPrMDxaaXVrWSbEz_uD9xqGY3Lvqbox5vfRYWFM8,2664
|
|
7
|
+
larkup-0.1.14.dist-info/RECORD,,
|