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.
@@ -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
+
@@ -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`).
@@ -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,4 @@
1
+ from .langchain import AksearchRetriever, AksearchSearchTool
2
+ from .llama_index import AksearchReader
3
+
4
+ __all__ = ["AksearchRetriever", "AksearchSearchTool", "AksearchReader"]
@@ -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"