aksearch 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.
- aksearch-0.1.0/.gitignore +68 -0
- aksearch-0.1.0/PKG-INFO +137 -0
- aksearch-0.1.0/README.md +110 -0
- aksearch-0.1.0/aksearch/__init__.py +27 -0
- aksearch-0.1.0/aksearch/client.py +185 -0
- aksearch-0.1.0/aksearch/exceptions.py +23 -0
- aksearch-0.1.0/aksearch/integrations/__init__.py +4 -0
- aksearch-0.1.0/aksearch/integrations/langchain.py +129 -0
- aksearch-0.1.0/aksearch/integrations/llama_index.py +60 -0
- aksearch-0.1.0/aksearch/models.py +73 -0
- aksearch-0.1.0/pyproject.toml +42 -0
- aksearch-0.1.0/tests/test_client.py +130 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# The directory Mix will write compiled artifacts to.
|
|
2
|
+
/_build/
|
|
3
|
+
|
|
4
|
+
# If you run "mix test --cover", coverage assets end up here.
|
|
5
|
+
/cover/
|
|
6
|
+
|
|
7
|
+
# The directory Mix downloads your dependencies sources to.
|
|
8
|
+
/deps/
|
|
9
|
+
|
|
10
|
+
# Where third-party dependencies like ExDoc output generated docs.
|
|
11
|
+
/doc/
|
|
12
|
+
|
|
13
|
+
# Temporary files, for example, from tests.
|
|
14
|
+
/tmp/
|
|
15
|
+
|
|
16
|
+
# If the VM crashes, it generates a dump, let's ignore it too.
|
|
17
|
+
erl_crash.dump
|
|
18
|
+
|
|
19
|
+
# Also ignore archive artifacts (built via "mix archive.build").
|
|
20
|
+
*.ez
|
|
21
|
+
|
|
22
|
+
# Ignore package tarball (built via "mix hex.build").
|
|
23
|
+
aksearch-*.tar
|
|
24
|
+
|
|
25
|
+
priv/repo/*.db
|
|
26
|
+
priv/repo/*.db-shm
|
|
27
|
+
priv/repo/*.db-wal
|
|
28
|
+
|
|
29
|
+
# Python
|
|
30
|
+
__pycache__/
|
|
31
|
+
*.py[cod]
|
|
32
|
+
*$py.class
|
|
33
|
+
*.so
|
|
34
|
+
.Python
|
|
35
|
+
.venv/
|
|
36
|
+
env/
|
|
37
|
+
venv/
|
|
38
|
+
ENV/
|
|
39
|
+
build/
|
|
40
|
+
develop-eggs/
|
|
41
|
+
dist/
|
|
42
|
+
downloads/
|
|
43
|
+
eggs/
|
|
44
|
+
.eggs/
|
|
45
|
+
parts/
|
|
46
|
+
sdist/
|
|
47
|
+
var/
|
|
48
|
+
wheels/
|
|
49
|
+
*.egg-info/
|
|
50
|
+
.installed.cfg
|
|
51
|
+
*.egg
|
|
52
|
+
.pytest_cache/
|
|
53
|
+
.coverage
|
|
54
|
+
htmlcov/
|
|
55
|
+
|
|
56
|
+
# Editor & OS
|
|
57
|
+
.DS_Store
|
|
58
|
+
.idea/
|
|
59
|
+
.vscode/
|
|
60
|
+
*.swp
|
|
61
|
+
*.swo
|
|
62
|
+
|
|
63
|
+
# Environment variables & secrets
|
|
64
|
+
.env
|
|
65
|
+
.env.*
|
|
66
|
+
!.env.example
|
|
67
|
+
*.env
|
|
68
|
+
|
aksearch-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: aksearch
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK and LangChain/LlamaIndex integration for Aksearch - Fast Korean Search with Clean Markdown Extraction
|
|
5
|
+
Author: Aksearch Team
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Requires-Dist: httpx>=0.24.0
|
|
16
|
+
Provides-Extra: all
|
|
17
|
+
Requires-Dist: langchain-core>=0.1.0; extra == 'all'
|
|
18
|
+
Requires-Dist: llama-index-core>=0.10.0; extra == 'all'
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
22
|
+
Provides-Extra: langchain
|
|
23
|
+
Requires-Dist: langchain-core>=0.1.0; extra == 'langchain'
|
|
24
|
+
Provides-Extra: llama-index
|
|
25
|
+
Requires-Dist: llama-index-core>=0.10.0; extra == 'llama-index'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# Aksearch Python SDK
|
|
29
|
+
|
|
30
|
+
Official Python client and LangChain / LlamaIndex integrations for **Aksearch** — a high-performance search aggregation engine tailored for Korean and global web queries with on-demand Clean Markdown extraction.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 📦 Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install aksearch
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
For LangChain or LlamaIndex support:
|
|
41
|
+
```bash
|
|
42
|
+
pip install "aksearch[langchain]"
|
|
43
|
+
# or
|
|
44
|
+
pip install "aksearch[llama-index]"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 🚀 Quickstart
|
|
50
|
+
|
|
51
|
+
### 1. Basic Search
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from aksearch import AksearchClient
|
|
55
|
+
|
|
56
|
+
client = AksearchClient(api_key="YOUR_AKSEARCH_API_KEY")
|
|
57
|
+
|
|
58
|
+
# Basic meta search (Title, URL, Snippet)
|
|
59
|
+
response = client.search("엘릭서 기본 문법")
|
|
60
|
+
|
|
61
|
+
for item in response:
|
|
62
|
+
print(f"[{item.source}] {item.title} -> {item.url}")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 2. LLM-Friendly Clean Markdown Extraction
|
|
66
|
+
|
|
67
|
+
Set `include_content=True` to receive cleaned, boilerplate-free Markdown text extracted from the top authoritative pages:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
response = client.search(
|
|
71
|
+
"엘릭서 기본 문법",
|
|
72
|
+
include_content=True,
|
|
73
|
+
max_content_items=3 # Fetch full clean markdown for top 3 URLs
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
for item in response:
|
|
77
|
+
print(f"Title: {item.title}")
|
|
78
|
+
if item.content:
|
|
79
|
+
print(f"Clean Content:\n{item.content[:300]}...\n")
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 🦜 LangChain Integration
|
|
85
|
+
|
|
86
|
+
### As a Retriever (RAG Pipelines)
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from aksearch.integrations import AksearchRetriever
|
|
90
|
+
|
|
91
|
+
retriever = AksearchRetriever(
|
|
92
|
+
api_key="YOUR_AKSEARCH_API_KEY",
|
|
93
|
+
include_content=True,
|
|
94
|
+
max_content_items=3
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Returns a list of langchain_core.documents.Document
|
|
98
|
+
docs = retriever.invoke("최신 인공지능 트렌드")
|
|
99
|
+
print(docs[0].page_content)
|
|
100
|
+
print(docs[0].metadata["source"])
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### As an Agent Tool
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from aksearch.integrations import AksearchSearchTool
|
|
107
|
+
|
|
108
|
+
search_tool = AksearchSearchTool(api_key="YOUR_AKSEARCH_API_KEY")
|
|
109
|
+
|
|
110
|
+
# Pass to any LangChain ReAct or Tool-calling agent:
|
|
111
|
+
# tools = [search_tool]
|
|
112
|
+
# agent = create_react_agent(model, tools, prompt)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 🦙 LlamaIndex Integration
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from aksearch.integrations import AksearchReader
|
|
121
|
+
from llama_index.core import VectorStoreIndex
|
|
122
|
+
|
|
123
|
+
reader = AksearchReader(api_key="YOUR_AKSEARCH_API_KEY")
|
|
124
|
+
documents = reader.load_data("한국 역사 요약", include_content=True)
|
|
125
|
+
|
|
126
|
+
index = VectorStoreIndex.from_documents(documents)
|
|
127
|
+
query_engine = index.as_query_engine()
|
|
128
|
+
response = query_engine.query("조선 건국 과정 요약해줘")
|
|
129
|
+
print(response)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## ⚙️ Environment Variables
|
|
135
|
+
|
|
136
|
+
- `AKSEARCH_API_KEY`: Fallback API key when not explicitly passed to the client.
|
|
137
|
+
- `AKSEARCH_BASE_URL`: Custom API endpoint (default: `http://localhost:4000/api`).
|
aksearch-0.1.0/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Aksearch Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python client and LangChain / LlamaIndex integrations for **Aksearch** — a high-performance search aggregation engine tailored for Korean and global web queries with on-demand Clean Markdown extraction.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 📦 Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install aksearch
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For LangChain or LlamaIndex support:
|
|
14
|
+
```bash
|
|
15
|
+
pip install "aksearch[langchain]"
|
|
16
|
+
# or
|
|
17
|
+
pip install "aksearch[llama-index]"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 🚀 Quickstart
|
|
23
|
+
|
|
24
|
+
### 1. Basic Search
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from aksearch import AksearchClient
|
|
28
|
+
|
|
29
|
+
client = AksearchClient(api_key="YOUR_AKSEARCH_API_KEY")
|
|
30
|
+
|
|
31
|
+
# Basic meta search (Title, URL, Snippet)
|
|
32
|
+
response = client.search("엘릭서 기본 문법")
|
|
33
|
+
|
|
34
|
+
for item in response:
|
|
35
|
+
print(f"[{item.source}] {item.title} -> {item.url}")
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. LLM-Friendly Clean Markdown Extraction
|
|
39
|
+
|
|
40
|
+
Set `include_content=True` to receive cleaned, boilerplate-free Markdown text extracted from the top authoritative pages:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
response = client.search(
|
|
44
|
+
"엘릭서 기본 문법",
|
|
45
|
+
include_content=True,
|
|
46
|
+
max_content_items=3 # Fetch full clean markdown for top 3 URLs
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
for item in response:
|
|
50
|
+
print(f"Title: {item.title}")
|
|
51
|
+
if item.content:
|
|
52
|
+
print(f"Clean Content:\n{item.content[:300]}...\n")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 🦜 LangChain Integration
|
|
58
|
+
|
|
59
|
+
### As a Retriever (RAG Pipelines)
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from aksearch.integrations import AksearchRetriever
|
|
63
|
+
|
|
64
|
+
retriever = AksearchRetriever(
|
|
65
|
+
api_key="YOUR_AKSEARCH_API_KEY",
|
|
66
|
+
include_content=True,
|
|
67
|
+
max_content_items=3
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Returns a list of langchain_core.documents.Document
|
|
71
|
+
docs = retriever.invoke("최신 인공지능 트렌드")
|
|
72
|
+
print(docs[0].page_content)
|
|
73
|
+
print(docs[0].metadata["source"])
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### As an Agent Tool
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from aksearch.integrations import AksearchSearchTool
|
|
80
|
+
|
|
81
|
+
search_tool = AksearchSearchTool(api_key="YOUR_AKSEARCH_API_KEY")
|
|
82
|
+
|
|
83
|
+
# Pass to any LangChain ReAct or Tool-calling agent:
|
|
84
|
+
# tools = [search_tool]
|
|
85
|
+
# agent = create_react_agent(model, tools, prompt)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 🦙 LlamaIndex Integration
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from aksearch.integrations import AksearchReader
|
|
94
|
+
from llama_index.core import VectorStoreIndex
|
|
95
|
+
|
|
96
|
+
reader = AksearchReader(api_key="YOUR_AKSEARCH_API_KEY")
|
|
97
|
+
documents = reader.load_data("한국 역사 요약", include_content=True)
|
|
98
|
+
|
|
99
|
+
index = VectorStoreIndex.from_documents(documents)
|
|
100
|
+
query_engine = index.as_query_engine()
|
|
101
|
+
response = query_engine.query("조선 건국 과정 요약해줘")
|
|
102
|
+
print(response)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## ⚙️ Environment Variables
|
|
108
|
+
|
|
109
|
+
- `AKSEARCH_API_KEY`: Fallback API key when not explicitly passed to the client.
|
|
110
|
+
- `AKSEARCH_BASE_URL`: Custom API endpoint (default: `http://localhost:4000/api`).
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from .client import AksearchClient, AsyncAksearchClient
|
|
2
|
+
from .models import SearchResponse, SearchResult
|
|
3
|
+
from .exceptions import (
|
|
4
|
+
AksearchError,
|
|
5
|
+
AuthenticationError,
|
|
6
|
+
RateLimitError,
|
|
7
|
+
BadRequestError,
|
|
8
|
+
SearchError,
|
|
9
|
+
)
|
|
10
|
+
from .integrations import AksearchRetriever, AksearchSearchTool, AksearchReader
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"AksearchClient",
|
|
16
|
+
"AsyncAksearchClient",
|
|
17
|
+
"SearchResponse",
|
|
18
|
+
"SearchResult",
|
|
19
|
+
"AksearchError",
|
|
20
|
+
"AuthenticationError",
|
|
21
|
+
"RateLimitError",
|
|
22
|
+
"BadRequestError",
|
|
23
|
+
"SearchError",
|
|
24
|
+
"AksearchRetriever",
|
|
25
|
+
"AksearchSearchTool",
|
|
26
|
+
"AksearchReader",
|
|
27
|
+
]
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Optional, Dict, Any
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from .exceptions import (
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
RateLimitError,
|
|
8
|
+
BadRequestError,
|
|
9
|
+
SearchError,
|
|
10
|
+
AksearchError,
|
|
11
|
+
)
|
|
12
|
+
from .models import SearchResponse
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BaseClient:
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
api_key: Optional[str] = None,
|
|
19
|
+
base_url: Optional[str] = None,
|
|
20
|
+
timeout: float = 10.0,
|
|
21
|
+
):
|
|
22
|
+
self.api_key = api_key or os.environ.get("AKSEARCH_API_KEY")
|
|
23
|
+
if not self.api_key:
|
|
24
|
+
raise AuthenticationError(
|
|
25
|
+
"Aksearch API key is required. Pass `api_key='...'` or set the AKSEARCH_API_KEY environment variable."
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
raw_base_url = (
|
|
29
|
+
base_url
|
|
30
|
+
or os.environ.get("AKSEARCH_BASE_URL")
|
|
31
|
+
or "http://localhost:4000/api"
|
|
32
|
+
)
|
|
33
|
+
self.base_url = raw_base_url.rstrip("/") + "/"
|
|
34
|
+
self.timeout = timeout
|
|
35
|
+
|
|
36
|
+
def _build_headers(self) -> Dict[str, str]:
|
|
37
|
+
return {
|
|
38
|
+
"x-api-key": self.api_key,
|
|
39
|
+
"accept": "application/json",
|
|
40
|
+
"user-agent": "aksearch-python/0.1.0",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
def _build_params(
|
|
44
|
+
self,
|
|
45
|
+
query: str,
|
|
46
|
+
include_content: bool,
|
|
47
|
+
max_content_items: int,
|
|
48
|
+
**extra_params,
|
|
49
|
+
) -> Dict[str, Any]:
|
|
50
|
+
params = {
|
|
51
|
+
"q": query,
|
|
52
|
+
"include_content": "true" if include_content else "false",
|
|
53
|
+
"max_content_items": str(max(0, min(10, max_content_items))),
|
|
54
|
+
}
|
|
55
|
+
params.update(extra_params)
|
|
56
|
+
return params
|
|
57
|
+
|
|
58
|
+
def _handle_response(self, response: httpx.Response) -> Dict[str, Any]:
|
|
59
|
+
if response.status_code == 200:
|
|
60
|
+
return response.json()
|
|
61
|
+
elif response.status_code == 401:
|
|
62
|
+
raise AuthenticationError(
|
|
63
|
+
f"Unauthorized: {response.text or 'Invalid or missing API key.'}"
|
|
64
|
+
)
|
|
65
|
+
elif response.status_code == 429:
|
|
66
|
+
raise RateLimitError(
|
|
67
|
+
f"Rate limit exceeded: {response.text or 'Monthly quota reached.'}"
|
|
68
|
+
)
|
|
69
|
+
elif response.status_code == 400:
|
|
70
|
+
raise BadRequestError(f"Bad request: {response.text}")
|
|
71
|
+
else:
|
|
72
|
+
raise SearchError(
|
|
73
|
+
f"Search request failed with status {response.status_code}: {response.text}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class AksearchClient(BaseClient):
|
|
78
|
+
"""Synchronous client for Aksearch API."""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
api_key: Optional[str] = None,
|
|
83
|
+
base_url: Optional[str] = None,
|
|
84
|
+
timeout: float = 10.0,
|
|
85
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
86
|
+
):
|
|
87
|
+
super().__init__(api_key=api_key, base_url=base_url, timeout=timeout)
|
|
88
|
+
self._client = httpx.Client(
|
|
89
|
+
base_url=self.base_url,
|
|
90
|
+
headers=self._build_headers(),
|
|
91
|
+
timeout=self.timeout,
|
|
92
|
+
transport=transport,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def close(self):
|
|
96
|
+
self._client.close()
|
|
97
|
+
|
|
98
|
+
def __enter__(self):
|
|
99
|
+
return self
|
|
100
|
+
|
|
101
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
102
|
+
self.close()
|
|
103
|
+
|
|
104
|
+
def search(
|
|
105
|
+
self,
|
|
106
|
+
query: str,
|
|
107
|
+
include_content: bool = False,
|
|
108
|
+
max_content_items: int = 3,
|
|
109
|
+
**extra_params,
|
|
110
|
+
) -> SearchResponse:
|
|
111
|
+
"""
|
|
112
|
+
Execute a search query across Korean search engines with optional clean markdown extraction.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
query: The search term or question.
|
|
116
|
+
include_content: When True, fetches and extracts clean markdown body text for top URLs.
|
|
117
|
+
max_content_items: Number of top results to enrich with full clean content (default: 3, max: 10).
|
|
118
|
+
**extra_params: Additional query parameters.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
SearchResponse: Parsed results including title, url, snippet, and clean markdown content.
|
|
122
|
+
"""
|
|
123
|
+
params = self._build_params(
|
|
124
|
+
query=query,
|
|
125
|
+
include_content=include_content,
|
|
126
|
+
max_content_items=max_content_items,
|
|
127
|
+
**extra_params,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
response = self._client.get("search", params=params)
|
|
132
|
+
data = self._handle_response(response)
|
|
133
|
+
return SearchResponse.from_dict(data)
|
|
134
|
+
except httpx.RequestError as exc:
|
|
135
|
+
raise AksearchError(f"Network error during search: {exc}") from exc
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class AsyncAksearchClient(BaseClient):
|
|
139
|
+
"""Asynchronous client for Aksearch API."""
|
|
140
|
+
|
|
141
|
+
def __init__(
|
|
142
|
+
self,
|
|
143
|
+
api_key: Optional[str] = None,
|
|
144
|
+
base_url: Optional[str] = None,
|
|
145
|
+
timeout: float = 10.0,
|
|
146
|
+
transport: Optional[httpx.AsyncBaseTransport] = None,
|
|
147
|
+
):
|
|
148
|
+
super().__init__(api_key=api_key, base_url=base_url, timeout=timeout)
|
|
149
|
+
self._client = httpx.AsyncClient(
|
|
150
|
+
base_url=self.base_url,
|
|
151
|
+
headers=self._build_headers(),
|
|
152
|
+
timeout=self.timeout,
|
|
153
|
+
transport=transport,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
async def close(self):
|
|
157
|
+
await self._client.aclose()
|
|
158
|
+
|
|
159
|
+
async def __aenter__(self):
|
|
160
|
+
return self
|
|
161
|
+
|
|
162
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
163
|
+
await self.close()
|
|
164
|
+
|
|
165
|
+
async def search(
|
|
166
|
+
self,
|
|
167
|
+
query: str,
|
|
168
|
+
include_content: bool = False,
|
|
169
|
+
max_content_items: int = 3,
|
|
170
|
+
**extra_params,
|
|
171
|
+
) -> SearchResponse:
|
|
172
|
+
"""Asynchronously execute a search query."""
|
|
173
|
+
params = self._build_params(
|
|
174
|
+
query=query,
|
|
175
|
+
include_content=include_content,
|
|
176
|
+
max_content_items=max_content_items,
|
|
177
|
+
**extra_params,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
response = await self._client.get("search", params=params)
|
|
182
|
+
data = self._handle_response(response)
|
|
183
|
+
return SearchResponse.from_dict(data)
|
|
184
|
+
except httpx.RequestError as exc:
|
|
185
|
+
raise AksearchError(f"Network error during search: {exc}") from exc
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class AksearchError(Exception):
|
|
2
|
+
"""Base exception for all Aksearch SDK errors."""
|
|
3
|
+
pass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AuthenticationError(AksearchError):
|
|
7
|
+
"""Raised when API key is missing, invalid, or unauthorized."""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RateLimitError(AksearchError):
|
|
12
|
+
"""Raised when the plan's search or API call limit is reached."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BadRequestError(AksearchError):
|
|
17
|
+
"""Raised when request parameters are invalid (e.g. empty query)."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SearchError(AksearchError):
|
|
22
|
+
"""Raised when the search engine backend encounters an internal error."""
|
|
23
|
+
pass
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
from typing import List, Optional, Any, Dict
|
|
2
|
+
from ..client import AksearchClient, AsyncAksearchClient
|
|
3
|
+
from ..models import SearchResponse
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AksearchRetriever:
|
|
7
|
+
"""
|
|
8
|
+
LangChain-compatible Retriever for Aksearch.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
retriever = AksearchRetriever(api_key="...", include_content=True)
|
|
12
|
+
docs = retriever.invoke("엘릭서 기본 문법")
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
api_key: Optional[str] = None,
|
|
18
|
+
base_url: Optional[str] = None,
|
|
19
|
+
include_content: bool = True,
|
|
20
|
+
max_content_items: int = 3,
|
|
21
|
+
client: Optional[AksearchClient] = None,
|
|
22
|
+
async_client: Optional[AsyncAksearchClient] = None,
|
|
23
|
+
):
|
|
24
|
+
self.include_content = include_content
|
|
25
|
+
self.max_content_items = max_content_items
|
|
26
|
+
self.client = client or AksearchClient(api_key=api_key, base_url=base_url)
|
|
27
|
+
self._async_client = async_client
|
|
28
|
+
|
|
29
|
+
def _to_langchain_doc(self, result):
|
|
30
|
+
try:
|
|
31
|
+
from langchain_core.documents import Document
|
|
32
|
+
except ImportError:
|
|
33
|
+
# Fallback lightweight Document representation if langchain_core is not installed
|
|
34
|
+
class Document:
|
|
35
|
+
def __init__(self, page_content: str, metadata: dict):
|
|
36
|
+
self.page_content = page_content
|
|
37
|
+
self.metadata = metadata
|
|
38
|
+
|
|
39
|
+
def __repr__(self):
|
|
40
|
+
return f"Document(page_content={self.page_content[:50]!r}..., metadata={self.metadata})"
|
|
41
|
+
|
|
42
|
+
text = result.content or result.snippet or result.title
|
|
43
|
+
metadata = {
|
|
44
|
+
"title": result.title,
|
|
45
|
+
"source": result.url,
|
|
46
|
+
"url": result.url,
|
|
47
|
+
"score": result.score,
|
|
48
|
+
"engine": result.source,
|
|
49
|
+
**result.metadata,
|
|
50
|
+
}
|
|
51
|
+
return Document(page_content=text, metadata=metadata)
|
|
52
|
+
|
|
53
|
+
def get_relevant_documents(self, query: str) -> List[Any]:
|
|
54
|
+
response: SearchResponse = self.client.search(
|
|
55
|
+
query=query,
|
|
56
|
+
include_content=self.include_content,
|
|
57
|
+
max_content_items=self.max_content_items,
|
|
58
|
+
)
|
|
59
|
+
return [self._to_langchain_doc(r) for r in response.results]
|
|
60
|
+
|
|
61
|
+
def invoke(self, query: str, config: Optional[Dict[str, Any]] = None) -> List[Any]:
|
|
62
|
+
"""LangChain Runnable protocol compatibility."""
|
|
63
|
+
return self.get_relevant_documents(query)
|
|
64
|
+
|
|
65
|
+
async def aget_relevant_documents(self, query: str) -> List[Any]:
|
|
66
|
+
if not self._async_client:
|
|
67
|
+
self._async_client = AsyncAksearchClient(
|
|
68
|
+
api_key=self.client.api_key, base_url=self.client.base_url
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
response: SearchResponse = await self._async_client.search(
|
|
72
|
+
query=query,
|
|
73
|
+
include_content=self.include_content,
|
|
74
|
+
max_content_items=self.max_content_items,
|
|
75
|
+
)
|
|
76
|
+
return [self._to_langchain_doc(r) for r in response.results]
|
|
77
|
+
|
|
78
|
+
async def ainvoke(self, query: str, config: Optional[Dict[str, Any]] = None) -> List[Any]:
|
|
79
|
+
"""LangChain Async Runnable protocol compatibility."""
|
|
80
|
+
return await self.aget_relevant_documents(query)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class AksearchSearchTool:
|
|
84
|
+
"""
|
|
85
|
+
LangChain agent tool for searching the web and retrieving clean markdown content.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
name: str = "aksearch"
|
|
89
|
+
description: str = (
|
|
90
|
+
"Search the Korean and global web for real-time information, news, blogs, and articles. "
|
|
91
|
+
"Returns clean markdown content extracted from top authoritative websites."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
api_key: Optional[str] = None,
|
|
97
|
+
base_url: Optional[str] = None,
|
|
98
|
+
include_content: bool = True,
|
|
99
|
+
max_content_items: int = 3,
|
|
100
|
+
):
|
|
101
|
+
self.client = AksearchClient(api_key=api_key, base_url=base_url)
|
|
102
|
+
self.include_content = include_content
|
|
103
|
+
self.max_content_items = max_content_items
|
|
104
|
+
|
|
105
|
+
def run(self, query: str) -> str:
|
|
106
|
+
"""Runs the search and formats results into an LLM-friendly context block."""
|
|
107
|
+
response = self.client.search(
|
|
108
|
+
query=query,
|
|
109
|
+
include_content=self.include_content,
|
|
110
|
+
max_content_items=self.max_content_items,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if not response.results:
|
|
114
|
+
return f"No search results found for query: {query}"
|
|
115
|
+
|
|
116
|
+
output_parts = []
|
|
117
|
+
for idx, item in enumerate(response.results, 1):
|
|
118
|
+
text_body = item.content or item.snippet or "No content available."
|
|
119
|
+
output_parts.append(
|
|
120
|
+
f"### [{idx}] {item.title}\n"
|
|
121
|
+
f"- **URL**: {item.url}\n"
|
|
122
|
+
f"- **Source**: {item.source or 'web'}\n\n"
|
|
123
|
+
f"{text_body}\n"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
return "\n---\n".join(output_parts)
|
|
127
|
+
|
|
128
|
+
def __call__(self, query: str) -> str:
|
|
129
|
+
return self.run(query)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from typing import List, Optional, Any
|
|
2
|
+
from ..client import AksearchClient
|
|
3
|
+
from ..models import SearchResponse
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AksearchReader:
|
|
7
|
+
"""
|
|
8
|
+
LlamaIndex-compatible Reader for Aksearch.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
reader = AksearchReader(api_key="...")
|
|
12
|
+
documents = reader.load_data("한국 최신 뉴스", include_content=True)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
api_key: Optional[str] = None,
|
|
18
|
+
base_url: Optional[str] = None,
|
|
19
|
+
client: Optional[AksearchClient] = None,
|
|
20
|
+
):
|
|
21
|
+
self.client = client or AksearchClient(api_key=api_key, base_url=base_url)
|
|
22
|
+
|
|
23
|
+
def _to_llama_doc(self, result):
|
|
24
|
+
try:
|
|
25
|
+
from llama_index.core import Document
|
|
26
|
+
except ImportError:
|
|
27
|
+
# Fallback lightweight representation if llama_index is not installed
|
|
28
|
+
class Document:
|
|
29
|
+
def __init__(self, text: str, extra_info: dict):
|
|
30
|
+
self.text = text
|
|
31
|
+
self.extra_info = extra_info
|
|
32
|
+
|
|
33
|
+
def __repr__(self):
|
|
34
|
+
return f"Document(text={self.text[:50]!r}..., extra_info={self.extra_info})"
|
|
35
|
+
|
|
36
|
+
text = result.content or result.snippet or result.title
|
|
37
|
+
extra_info = {
|
|
38
|
+
"title": result.title,
|
|
39
|
+
"url": result.url,
|
|
40
|
+
"source": result.source,
|
|
41
|
+
"score": result.score,
|
|
42
|
+
**result.metadata,
|
|
43
|
+
}
|
|
44
|
+
return Document(text=text, extra_info=extra_info)
|
|
45
|
+
|
|
46
|
+
def load_data(
|
|
47
|
+
self,
|
|
48
|
+
query: str,
|
|
49
|
+
include_content: bool = True,
|
|
50
|
+
max_content_items: int = 3,
|
|
51
|
+
**kwargs,
|
|
52
|
+
) -> List[Any]:
|
|
53
|
+
"""Loads search results as LlamaIndex Documents."""
|
|
54
|
+
response: SearchResponse = self.client.search(
|
|
55
|
+
query=query,
|
|
56
|
+
include_content=include_content,
|
|
57
|
+
max_content_items=max_content_items,
|
|
58
|
+
**kwargs,
|
|
59
|
+
)
|
|
60
|
+
return [self._to_llama_doc(r) for r in response.results]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import List, Optional, Dict, Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class SearchResult:
|
|
7
|
+
"""Represents an individual search result, optionally enriched with clean markdown content."""
|
|
8
|
+
title: str
|
|
9
|
+
url: str
|
|
10
|
+
snippet: Optional[str] = None
|
|
11
|
+
content: Optional[str] = None
|
|
12
|
+
source: Optional[str] = None
|
|
13
|
+
score: Optional[float] = None
|
|
14
|
+
duplicate_sources: List[str] = field(default_factory=list)
|
|
15
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def from_dict(cls, data: Dict[str, Any]) -> "SearchResult":
|
|
19
|
+
return cls(
|
|
20
|
+
title=data.get("title", ""),
|
|
21
|
+
url=data.get("url", ""),
|
|
22
|
+
snippet=data.get("snippet"),
|
|
23
|
+
content=data.get("content"),
|
|
24
|
+
source=data.get("source"),
|
|
25
|
+
score=data.get("score"),
|
|
26
|
+
duplicate_sources=data.get("duplicate_sources", []),
|
|
27
|
+
metadata=data.get("metadata", {})
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
31
|
+
return {
|
|
32
|
+
"title": self.title,
|
|
33
|
+
"url": self.url,
|
|
34
|
+
"snippet": self.snippet,
|
|
35
|
+
"content": self.content,
|
|
36
|
+
"source": self.source,
|
|
37
|
+
"score": self.score,
|
|
38
|
+
"duplicate_sources": self.duplicate_sources,
|
|
39
|
+
"metadata": self.metadata
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class SearchResponse:
|
|
45
|
+
"""Represents the complete response from an Aksearch query."""
|
|
46
|
+
query: str
|
|
47
|
+
results: List[SearchResult]
|
|
48
|
+
counts: Dict[str, int] = field(default_factory=dict)
|
|
49
|
+
cache_status: Optional[str] = None
|
|
50
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def from_dict(cls, data: Dict[str, Any]) -> "SearchResponse":
|
|
54
|
+
results = [
|
|
55
|
+
SearchResult.from_dict(item)
|
|
56
|
+
for item in data.get("results", [])
|
|
57
|
+
]
|
|
58
|
+
return cls(
|
|
59
|
+
query=data.get("query", ""),
|
|
60
|
+
results=results,
|
|
61
|
+
counts=data.get("counts", {}),
|
|
62
|
+
cache_status=data.get("cache_status"),
|
|
63
|
+
raw=data
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def __iter__(self):
|
|
67
|
+
return iter(self.results)
|
|
68
|
+
|
|
69
|
+
def __len__(self):
|
|
70
|
+
return len(self.results)
|
|
71
|
+
|
|
72
|
+
def __getitem__(self, index):
|
|
73
|
+
return self.results[index]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "aksearch"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python SDK and LangChain/LlamaIndex integration for Aksearch - Fast Korean Search with Clean Markdown Extraction"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Aksearch Team" }
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.9",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
22
|
+
"Topic :: Internet :: WWW/HTTP :: Indexing/Search",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"httpx>=0.24.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
langchain = [
|
|
30
|
+
"langchain-core>=0.1.0",
|
|
31
|
+
]
|
|
32
|
+
llama-index = [
|
|
33
|
+
"llama-index-core>=0.10.0",
|
|
34
|
+
]
|
|
35
|
+
all = [
|
|
36
|
+
"langchain-core>=0.1.0",
|
|
37
|
+
"llama-index-core>=0.10.0",
|
|
38
|
+
]
|
|
39
|
+
dev = [
|
|
40
|
+
"pytest>=7.0.0",
|
|
41
|
+
"pytest-asyncio>=0.21.0",
|
|
42
|
+
]
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import httpx
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from aksearch import (
|
|
7
|
+
AksearchClient,
|
|
8
|
+
AsyncAksearchClient,
|
|
9
|
+
AuthenticationError,
|
|
10
|
+
RateLimitError,
|
|
11
|
+
BadRequestError,
|
|
12
|
+
)
|
|
13
|
+
from aksearch.integrations import AksearchRetriever, AksearchSearchTool, AksearchReader
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def mock_search_response():
|
|
18
|
+
return {
|
|
19
|
+
"query": "elixir",
|
|
20
|
+
"results": [
|
|
21
|
+
{
|
|
22
|
+
"title": "Elixir Programming",
|
|
23
|
+
"url": "https://elixir-lang.org",
|
|
24
|
+
"snippet": "Elixir is a dynamic, functional language.",
|
|
25
|
+
"content": "# Elixir Overview\n\nElixir is built on the BEAM VM.",
|
|
26
|
+
"source": "google",
|
|
27
|
+
"score": 100,
|
|
28
|
+
"metadata": {"freshness": 10}
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"counts": {"google": 1},
|
|
32
|
+
"cache_status": "fresh"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_missing_api_key_raises_auth_error(monkeypatch):
|
|
37
|
+
monkeypatch.delenv("AKSEARCH_API_KEY", raising=False)
|
|
38
|
+
with pytest.raises(AuthenticationError):
|
|
39
|
+
AksearchClient()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_search_success(mock_search_response):
|
|
43
|
+
def handler(request: httpx.Request):
|
|
44
|
+
assert request.url.path == "/api/search"
|
|
45
|
+
assert request.url.params["q"] == "elixir"
|
|
46
|
+
assert request.url.params["include_content"] == "true"
|
|
47
|
+
assert request.url.params["max_content_items"] == "3"
|
|
48
|
+
assert request.headers["x-api-key"] == "test_key"
|
|
49
|
+
return httpx.Response(200, json=mock_search_response)
|
|
50
|
+
|
|
51
|
+
transport = httpx.MockTransport(handler)
|
|
52
|
+
client = AksearchClient(api_key="test_key", transport=transport)
|
|
53
|
+
|
|
54
|
+
response = client.search("elixir", include_content=True, max_content_items=3)
|
|
55
|
+
|
|
56
|
+
assert response.query == "elixir"
|
|
57
|
+
assert len(response) == 1
|
|
58
|
+
item = response[0]
|
|
59
|
+
assert item.title == "Elixir Programming"
|
|
60
|
+
assert item.url == "https://elixir-lang.org"
|
|
61
|
+
assert item.content == "# Elixir Overview\n\nElixir is built on the BEAM VM."
|
|
62
|
+
assert item.source == "google"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_error_status_codes():
|
|
66
|
+
transport_401 = httpx.MockTransport(lambda _: httpx.Response(401, text="Invalid API key"))
|
|
67
|
+
client = AksearchClient(api_key="bad_key", transport=transport_401)
|
|
68
|
+
with pytest.raises(AuthenticationError):
|
|
69
|
+
client.search("test")
|
|
70
|
+
|
|
71
|
+
transport_429 = httpx.MockTransport(lambda _: httpx.Response(429, text="Monthly quota reached"))
|
|
72
|
+
client = AksearchClient(api_key="test_key", transport=transport_429)
|
|
73
|
+
with pytest.raises(RateLimitError):
|
|
74
|
+
client.search("test")
|
|
75
|
+
|
|
76
|
+
transport_400 = httpx.MockTransport(lambda _: httpx.Response(400, text="Empty query"))
|
|
77
|
+
client = AksearchClient(api_key="test_key", transport=transport_400)
|
|
78
|
+
with pytest.raises(BadRequestError):
|
|
79
|
+
client.search("")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@pytest.mark.asyncio
|
|
83
|
+
async def test_async_search(mock_search_response):
|
|
84
|
+
def handler(request: httpx.Request):
|
|
85
|
+
return httpx.Response(200, json=mock_search_response)
|
|
86
|
+
|
|
87
|
+
transport = httpx.MockTransport(handler)
|
|
88
|
+
async with AsyncAksearchClient(api_key="test_key", transport=transport) as client:
|
|
89
|
+
response = await client.search("elixir")
|
|
90
|
+
assert len(response) == 1
|
|
91
|
+
assert response[0].title == "Elixir Programming"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_langchain_retriever(mock_search_response):
|
|
95
|
+
transport = httpx.MockTransport(lambda _: httpx.Response(200, json=mock_search_response))
|
|
96
|
+
client = AksearchClient(api_key="test_key", transport=transport)
|
|
97
|
+
|
|
98
|
+
retriever = AksearchRetriever(client=client, include_content=True)
|
|
99
|
+
docs = retriever.invoke("elixir")
|
|
100
|
+
|
|
101
|
+
assert len(docs) == 1
|
|
102
|
+
doc = docs[0]
|
|
103
|
+
assert doc.page_content == "# Elixir Overview\n\nElixir is built on the BEAM VM."
|
|
104
|
+
assert doc.metadata["title"] == "Elixir Programming"
|
|
105
|
+
assert doc.metadata["source"] == "https://elixir-lang.org"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_langchain_search_tool(mock_search_response):
|
|
109
|
+
transport = httpx.MockTransport(lambda _: httpx.Response(200, json=mock_search_response))
|
|
110
|
+
tool = AksearchSearchTool(api_key="test_key", base_url="http://localhost:4000/api")
|
|
111
|
+
tool.client._client = httpx.Client(base_url="http://localhost:4000/api/", transport=transport)
|
|
112
|
+
|
|
113
|
+
output = tool.run("elixir")
|
|
114
|
+
assert "### [1] Elixir Programming" in output
|
|
115
|
+
assert "https://elixir-lang.org" in output
|
|
116
|
+
assert "Elixir is built on the BEAM VM." in output
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_llama_index_reader(mock_search_response):
|
|
120
|
+
transport = httpx.MockTransport(lambda _: httpx.Response(200, json=mock_search_response))
|
|
121
|
+
client = AksearchClient(api_key="test_key", transport=transport)
|
|
122
|
+
|
|
123
|
+
reader = AksearchReader(client=client)
|
|
124
|
+
docs = reader.load_data("elixir", include_content=True)
|
|
125
|
+
|
|
126
|
+
assert len(docs) == 1
|
|
127
|
+
doc = docs[0]
|
|
128
|
+
assert doc.text == "# Elixir Overview\n\nElixir is built on the BEAM VM."
|
|
129
|
+
assert doc.extra_info["title"] == "Elixir Programming"
|
|
130
|
+
assert doc.extra_info["url"] == "https://elixir-lang.org"
|