unshadow 0.2.0__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.
@@ -0,0 +1,5 @@
1
+ """Optional LangChain retriever. Install ``langchain-core`` before importing."""
2
+
3
+ from langchain_unshadow.retriever import UnshadowRetriever
4
+
5
+ __all__ = ["UnshadowRetriever"]
@@ -0,0 +1,35 @@
1
+ """LangChain retriever over the Unshadow Engine client."""
2
+
3
+ from typing import Any
4
+
5
+ from langchain_core.callbacks import CallbackManagerForRetrieverRun
6
+ from langchain_core.documents import Document
7
+ from langchain_core.retrievers import BaseRetriever
8
+
9
+ from unshadow.documents import documents_from_context, documents_from_search
10
+
11
+
12
+ class UnshadowRetriever(BaseRetriever):
13
+ """Search Unshadow Engine. ``prefer_context`` uses ``POST /context``."""
14
+
15
+ client: Any
16
+ k: int = 8
17
+ prefer_context: bool = False
18
+
19
+ def _get_relevant_documents(
20
+ self,
21
+ query: str,
22
+ *,
23
+ run_manager: CallbackManagerForRetrieverRun,
24
+ ) -> list[Document]:
25
+ del run_manager
26
+ if self.prefer_context:
27
+ body = self.client.context(query, search_limit=self.k)
28
+ rows = documents_from_context(str(body.get("context") or ""), body.get("sources"))
29
+ else:
30
+ body = self.client.search(query, limit=self.k)
31
+ rows = documents_from_search(body)
32
+ return [
33
+ Document(page_content=row["page_content"], metadata=row["metadata"])
34
+ for row in rows[: self.k]
35
+ ]
@@ -0,0 +1,5 @@
1
+ """Optional LlamaIndex retriever. Install ``llama-index-core`` before importing."""
2
+
3
+ from llama_index_unshadow.retriever import UnshadowRetriever
4
+
5
+ __all__ = ["UnshadowRetriever"]
@@ -0,0 +1,32 @@
1
+ """LlamaIndex retriever over the Unshadow Engine client."""
2
+
3
+ from typing import Any
4
+
5
+ from llama_index.core.retrievers import BaseRetriever
6
+ from llama_index.core.schema import NodeWithScore, QueryBundle, TextNode
7
+
8
+ from unshadow.documents import documents_from_context, documents_from_search
9
+
10
+
11
+ class UnshadowRetriever(BaseRetriever):
12
+ """Search Unshadow Engine. ``prefer_context`` uses ``POST /context``."""
13
+
14
+ def __init__(self, client: Any, k: int = 8, prefer_context: bool = False, **kwargs: Any) -> None:
15
+ self._client = client
16
+ self._k = k
17
+ self._prefer_context = prefer_context
18
+ super().__init__(**kwargs)
19
+
20
+ def _retrieve(self, query_bundle: QueryBundle) -> list[NodeWithScore]:
21
+ query = query_bundle.query_str
22
+ if self._prefer_context:
23
+ body = self._client.context(query, search_limit=self._k)
24
+ rows = documents_from_context(str(body.get("context") or ""), body.get("sources"))
25
+ else:
26
+ body = self._client.search(query, limit=self._k)
27
+ rows = documents_from_search(body)
28
+ nodes: list[NodeWithScore] = []
29
+ for row in rows[: self._k]:
30
+ node = TextNode(text=row["page_content"], metadata=row["metadata"])
31
+ nodes.append(NodeWithScore(node=node, score=1.0))
32
+ return nodes
unshadow/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """Public Unshadow Python clients.
2
+
3
+ ``Unshadow`` is Engine HTTP (``/context``, ``/search``, ``/extract``, ``/forget-memories``, ``/profile``).
4
+ ``UnshadowBank`` is the agent bank (``/lattice/*``).
5
+ """
6
+
7
+ from unshadow.bank import UnshadowBank
8
+ from unshadow.client import Unshadow
9
+ from unshadow.http import UnshadowHttpError
10
+
11
+ __all__ = ["Unshadow", "UnshadowBank", "UnshadowHttpError"]
unshadow/bank.py ADDED
@@ -0,0 +1,127 @@
1
+ """Agent-bank client. HTTP paths stay ``/lattice/*``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Mapping, Sequence
6
+
7
+ from unshadow.http import UnshadowHttp, UnshadowHttpError
8
+
9
+
10
+ RETAIN_MAX_CHARS = 32_000
11
+
12
+
13
+ class UnshadowBank(UnshadowHttp):
14
+ def remember(
15
+ self,
16
+ text: str,
17
+ *,
18
+ conversation_key: str = "default",
19
+ prev_hash: str | None = None,
20
+ source_type: str = "manual_entry",
21
+ source_url: str | None = None,
22
+ ) -> dict[str, Any]:
23
+ """Delta retain. Do not pass file contents; cite ``source_url`` and link Capture."""
24
+ clipped = text if len(text) <= RETAIN_MAX_CHARS else text[: RETAIN_MAX_CHARS - 1]
25
+ body: dict[str, Any] = {
26
+ "text": clipped,
27
+ "conversation_key": conversation_key,
28
+ "source_type": source_type,
29
+ }
30
+ if prev_hash:
31
+ body["prev_hash"] = prev_hash
32
+ if source_url:
33
+ body["source_url"] = source_url
34
+ try:
35
+ return self.post("/lattice/retain", body)
36
+ except UnshadowHttpError as exc:
37
+ if exc.status != 409:
38
+ raise
39
+ err = exc.body.get("error") if isinstance(exc.body, Mapping) else None
40
+ expected = err.get("expected_hash") if isinstance(err, Mapping) else None
41
+ if not expected or expected == prev_hash:
42
+ raise
43
+ body["prev_hash"] = expected
44
+ return self.post("/lattice/retain", body)
45
+
46
+ def retain(
47
+ self,
48
+ text: str,
49
+ *,
50
+ conversation_key: str = "default",
51
+ prev_hash: str | None = None,
52
+ source_type: str = "manual_entry",
53
+ source_url: str | None = None,
54
+ ) -> dict[str, Any]:
55
+ return self.remember(
56
+ text,
57
+ conversation_key=conversation_key,
58
+ prev_hash=prev_hash,
59
+ source_type=source_type,
60
+ source_url=source_url,
61
+ )
62
+
63
+ def recall(
64
+ self,
65
+ query: str = "",
66
+ *,
67
+ purpose: str = "answer",
68
+ memory_ids: Sequence[str] | None = None,
69
+ limit: int | None = None,
70
+ trust: str | None = None,
71
+ token_budget: int | None = None,
72
+ format: str | None = None,
73
+ ) -> dict[str, Any]:
74
+ body: dict[str, Any] = {"query": query, "purpose": purpose}
75
+ if memory_ids is not None:
76
+ body["memory_ids"] = list(memory_ids)
77
+ if limit is not None:
78
+ body["limit"] = limit
79
+ if trust is not None:
80
+ body["trust"] = trust
81
+ if token_budget is not None:
82
+ body["token_budget"] = token_budget
83
+ if format is not None:
84
+ body["format"] = format
85
+ return self.post("/lattice/recall", body)
86
+
87
+ def context(
88
+ self,
89
+ query: str,
90
+ *,
91
+ trust: str = "advisory",
92
+ token_budget: int = 1200,
93
+ purpose: str = "answer",
94
+ ) -> dict[str, Any]:
95
+ return self.post(
96
+ "/lattice/context",
97
+ {
98
+ "query": query,
99
+ "trust": trust,
100
+ "token_budget": token_budget,
101
+ "purpose": purpose,
102
+ },
103
+ )
104
+
105
+ def authorize(
106
+ self,
107
+ memory_ids: Sequence[str],
108
+ *,
109
+ query: str = "",
110
+ ) -> dict[str, Any]:
111
+ """Fail-closed tool grounding. ``purpose`` is always ``tool_arg``."""
112
+ return self.recall(query, purpose="tool_arg", memory_ids=list(memory_ids))
113
+
114
+ def explain(self, operation_id: str) -> dict[str, Any]:
115
+ return self.post("/lattice/explain", {"operation_id": operation_id})
116
+
117
+ def reflect(self, query: str, **kwargs: Any) -> dict[str, Any]:
118
+ return self.post("/lattice/reflect", {"query": query, **kwargs})
119
+
120
+ def feedback(self, **kwargs: Any) -> dict[str, Any]:
121
+ return self.post("/lattice/feedback", kwargs)
122
+
123
+ def correct(self, memory_ids: Sequence[str]) -> dict[str, Any]:
124
+ return self.post("/lattice/correct", {"memory_ids": list(memory_ids)})
125
+
126
+ def forget(self, memory_ids: Sequence[str]) -> dict[str, Any]:
127
+ return self.post("/lattice/forget", {"memory_ids": list(memory_ids)})
unshadow/client.py ADDED
@@ -0,0 +1,96 @@
1
+ """Unshadow Engine client. Same HTTP as the TypeScript SDK and hosted MCP."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Mapping, Sequence
6
+ from urllib.parse import urlencode
7
+
8
+ from unshadow.http import UnshadowHttp
9
+
10
+
11
+ class Unshadow(UnshadowHttp):
12
+ def context(
13
+ self,
14
+ query: str | None = None,
15
+ *,
16
+ token_budget: int | None = None,
17
+ search_mode: str | None = None,
18
+ search_limit: int | None = None,
19
+ since: str | None = None,
20
+ until: str | None = None,
21
+ ) -> dict[str, Any]:
22
+ body: dict[str, Any] = {}
23
+ if query is not None:
24
+ body["query"] = query
25
+ if token_budget is not None:
26
+ body["token_budget"] = token_budget
27
+ if search_mode is not None:
28
+ body["search_mode"] = search_mode
29
+ if search_limit is not None:
30
+ body["search_limit"] = search_limit
31
+ if since is not None:
32
+ body["since"] = since
33
+ if until is not None:
34
+ body["until"] = until
35
+ return self.post("/context", body, retry=True)
36
+
37
+ def inject_context(self, query: str, **kwargs: Any) -> dict[str, Any]:
38
+ """Alias of ``context`` for prompt injection."""
39
+ return self.context(query, **kwargs)
40
+
41
+ def search(
42
+ self,
43
+ query: str,
44
+ *,
45
+ limit: int | None = None,
46
+ mode: str | None = None,
47
+ category: str | None = None,
48
+ pool: int | None = None,
49
+ ) -> dict[str, Any]:
50
+ params: dict[str, str] = {"query": query}
51
+ if limit is not None:
52
+ params["limit"] = str(limit)
53
+ if mode is not None:
54
+ params["mode"] = mode
55
+ if category is not None:
56
+ params["category"] = category
57
+ if pool is not None:
58
+ params["pool"] = str(pool)
59
+ return self.get(f"/search?{urlencode(params)}", retry=True)
60
+
61
+ def extract(
62
+ self,
63
+ text: str,
64
+ *,
65
+ source_type: str | None = None,
66
+ source_url: str | None = None,
67
+ conversation_id: str | None = None,
68
+ sandbox_id: str | None = None,
69
+ idempotency_key: str | None = None,
70
+ ) -> dict[str, Any]:
71
+ body: dict[str, Any] = {"text": text}
72
+ if source_type is not None:
73
+ body["source_type"] = source_type
74
+ if source_url is not None:
75
+ body["source_url"] = source_url
76
+ if conversation_id is not None:
77
+ body["conversation_id"] = conversation_id
78
+ if sandbox_id is not None:
79
+ body["sandbox_id"] = sandbox_id
80
+ headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
81
+ return self.post("/extract", body, headers=headers, retry=bool(idempotency_key))
82
+
83
+ def profile(self) -> dict[str, Any]:
84
+ return self.get("/profile", retry=True)
85
+
86
+ def forget(
87
+ self,
88
+ memory_ids: Sequence[str] | None = None,
89
+ *,
90
+ forget_all: bool = False,
91
+ ) -> dict[str, Any]:
92
+ if forget_all:
93
+ body: dict[str, Any] = {"forget_all": True}
94
+ else:
95
+ body = {"memory_ids": list(memory_ids or [])}
96
+ return self.post("/forget-memories", body)
unshadow/documents.py ADDED
@@ -0,0 +1,71 @@
1
+ """Turn Engine search and context payloads into retriever documents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any, Mapping
7
+
8
+
9
+ _MEM = re.compile(r"\[mem:([0-9a-fA-F-]{8,})\]")
10
+
11
+
12
+ def _fact(row: Any) -> tuple[str, dict[str, Any]] | None:
13
+ if not isinstance(row, Mapping):
14
+ return None
15
+ text = row.get("fact") or row.get("content") or row.get("statement") or row.get("text") or ""
16
+ if not isinstance(text, str) or not text.strip():
17
+ return None
18
+ metadata: dict[str, Any] = {}
19
+ for key in ("id", "memory_id", "category", "source_url", "source_type", "created_at", "supersedes", "superseded_by"):
20
+ if row.get(key) is not None:
21
+ metadata["id" if key == "memory_id" else key] = row[key]
22
+ return text, metadata
23
+
24
+
25
+ def documents_from_search(body: Mapping[str, Any]) -> list[dict[str, Any]]:
26
+ memories = body.get("memories") if isinstance(body.get("memories"), list) else []
27
+ docs = []
28
+ for row in memories:
29
+ parsed = _fact(row)
30
+ if parsed is None:
31
+ continue
32
+ page, metadata = parsed
33
+ docs.append({"page_content": page, "metadata": metadata})
34
+ if docs:
35
+ return docs
36
+ facts = body.get("facts") if isinstance(body.get("facts"), list) else []
37
+ return [
38
+ {"page_content": fact, "metadata": {}}
39
+ for fact in facts
40
+ if isinstance(fact, str) and fact.strip()
41
+ ]
42
+
43
+
44
+ def documents_from_context(context: str, sources: list[Mapping[str, Any]] | None = None) -> list[dict[str, Any]]:
45
+ rows = sources if isinstance(sources, list) else []
46
+ by_id: dict[str, Mapping[str, Any]] = {}
47
+ for row in rows:
48
+ if not isinstance(row, Mapping):
49
+ continue
50
+ memory_id = row.get("memory_id") or row.get("id")
51
+ if isinstance(memory_id, str) and memory_id:
52
+ by_id[memory_id] = row
53
+ docs: list[dict[str, Any]] = []
54
+ for chunk in re.split(r"\n{2,}|\n", context):
55
+ piece = chunk.strip()
56
+ if not piece:
57
+ continue
58
+ ids = _MEM.findall(piece)
59
+ page = _MEM.sub("", piece).strip()
60
+ if not page:
61
+ continue
62
+ metadata: dict[str, Any] = {"id": ids[0]} if ids else {}
63
+ source = by_id.get(ids[0]) if ids else None
64
+ if source:
65
+ for key in ("category", "source_type", "source_url", "supersedes", "superseded_by"):
66
+ if source.get(key) is not None:
67
+ metadata[key] = source[key]
68
+ docs.append({"page_content": page, "metadata": metadata})
69
+ if not docs and context.strip():
70
+ return [{"page_content": context.strip(), "metadata": {}}]
71
+ return docs
unshadow/http.py ADDED
@@ -0,0 +1,100 @@
1
+ """Shared HTTP for the Unshadow Python clients."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import time
7
+ import urllib.error
8
+ import urllib.request
9
+ from typing import Any, Mapping
10
+
11
+
12
+ DEFAULT_API_URL = "https://api.unshadow.dev/v1"
13
+ USER_AGENT = "unshadow-python/0.2.0"
14
+ RETRY_STATUSES = {429, 500, 502, 503, 504}
15
+
16
+
17
+ class UnshadowHttpError(Exception):
18
+ def __init__(self, status: int, body: Mapping[str, Any] | str) -> None:
19
+ self.status = status
20
+ self.body = body
21
+ super().__init__(f"Unshadow HTTP {status}: {body}")
22
+
23
+
24
+ def _parse_body(raw: bytes) -> dict[str, Any]:
25
+ if not raw:
26
+ return {}
27
+ try:
28
+ data = json.loads(raw.decode("utf-8"))
29
+ except (UnicodeDecodeError, json.JSONDecodeError):
30
+ return {"raw": raw.decode("utf-8", errors="replace")}
31
+ return data if isinstance(data, dict) else {"data": data}
32
+
33
+
34
+ class UnshadowHttp:
35
+ def __init__(
36
+ self,
37
+ api_key: str,
38
+ *,
39
+ base_url: str = DEFAULT_API_URL,
40
+ timeout: float = 30.0,
41
+ ) -> None:
42
+ self.api_key = api_key.strip()
43
+ self.base_url = (base_url or DEFAULT_API_URL).rstrip("/")
44
+ self.timeout = timeout
45
+
46
+ def request(
47
+ self,
48
+ method: str,
49
+ path: str,
50
+ payload: Mapping[str, Any] | None = None,
51
+ *,
52
+ headers: Mapping[str, str] | None = None,
53
+ retry: bool = False,
54
+ ) -> dict[str, Any]:
55
+ try:
56
+ return self._once(method, path, payload, headers)
57
+ except UnshadowHttpError as exc:
58
+ if not retry or exc.status not in RETRY_STATUSES:
59
+ raise
60
+ time.sleep(0.2)
61
+ return self._once(method, path, payload, headers)
62
+
63
+ def _once(
64
+ self,
65
+ method: str,
66
+ path: str,
67
+ payload: Mapping[str, Any] | None,
68
+ headers: Mapping[str, str] | None,
69
+ ) -> dict[str, Any]:
70
+ url = f"{self.base_url}{path if path.startswith('/') else '/' + path}"
71
+ data = None if payload is None else json.dumps(dict(payload)).encode("utf-8")
72
+ req_headers = {
73
+ "Authorization": f"Bearer {self.api_key}",
74
+ "Accept": "application/json",
75
+ "User-Agent": USER_AGENT,
76
+ }
77
+ if data is not None:
78
+ req_headers["Content-Type"] = "application/json"
79
+ if headers:
80
+ req_headers.update(dict(headers))
81
+ req = urllib.request.Request(url, data=data, method=method.upper(), headers=req_headers)
82
+ try:
83
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
84
+ return _parse_body(resp.read())
85
+ except urllib.error.HTTPError as exc:
86
+ body = _parse_body(exc.read() if exc.fp else b"")
87
+ raise UnshadowHttpError(int(exc.code), body) from exc
88
+
89
+ def post(
90
+ self,
91
+ path: str,
92
+ payload: Mapping[str, Any],
93
+ *,
94
+ headers: Mapping[str, str] | None = None,
95
+ retry: bool = False,
96
+ ) -> dict[str, Any]:
97
+ return self.request("POST", path, payload, headers=headers, retry=retry)
98
+
99
+ def get(self, path: str, *, retry: bool = False) -> dict[str, Any]:
100
+ return self.request("GET", path, retry=retry)
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.4
2
+ Name: unshadow
3
+ Version: 0.2.0
4
+ Summary: Python client for Unshadow Engine and agent banks.
5
+ Author-email: Unshadow <support@unshadow.dev>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://unshadow.dev
8
+ Project-URL: Documentation, https://unshadow.dev/docs/api/sdk
9
+ Project-URL: Repository, https://github.com/unshadow-ai/unshadow
10
+ Project-URL: Issues, https://github.com/unshadow-ai/unshadow/issues
11
+ Keywords: unshadow,memory,langchain,llamaindex,rag
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Provides-Extra: langchain
24
+ Requires-Dist: langchain-core>=0.3; extra == "langchain"
25
+ Provides-Extra: llamaindex
26
+ Requires-Dist: llama-index-core>=0.11; extra == "llamaindex"
27
+ Dynamic: license-file
28
+
29
+ # Unshadow (Python)
30
+
31
+ Stdlib client for **Unshadow Engine**, plus an agent-bank client. PyPI name: `unshadow`.
32
+
33
+ ```bash
34
+ pip install unshadow
35
+ ```
36
+
37
+ From this repo, before the PyPI upload:
38
+
39
+ ```bash
40
+ pip install ./packages/unshadow
41
+ ```
42
+
43
+ LangChain is optional:
44
+
45
+ ```bash
46
+ pip install "./packages/unshadow[langchain]"
47
+ ```
48
+
49
+ ## Engine
50
+
51
+ `Unshadow` matches the TypeScript SDK: `context`, `search`, `extract`, `forget`, `profile`.
52
+
53
+ ```python
54
+ from unshadow import Unshadow
55
+
56
+ unshadow = Unshadow(api_key="unshadow_…")
57
+ packed = unshadow.inject_context("What am I working on?", token_budget=1500)
58
+ hits = unshadow.search("pnpm", limit=8, category="preference")
59
+ unshadow.extract(
60
+ "User prefers pnpm",
61
+ source_type="ai_conversation",
62
+ idempotency_key="turn-1",
63
+ )
64
+ unshadow.forget(["11111111-1111-1111-1111-111111111111"])
65
+ ```
66
+
67
+ `extract(..., idempotency_key=)` sends `Idempotency-Key`. Reads retry once on 429/5xx. `extract` retries only when that key is set. A new fact can supersede, contradict, or coexist with an older one. Search rows include `supersedes` and `superseded_by`. `UnshadowRetriever` copies `id`, `category`, `source_type`, and those lists into document metadata. Filter with `unshadow.search("editor", category="preference")`.
68
+
69
+ ### LangChain
70
+
71
+ ```python
72
+ from langchain_unshadow import UnshadowRetriever
73
+ from unshadow import Unshadow
74
+
75
+ unshadow = Unshadow(api_key="unshadow_…")
76
+ retriever = UnshadowRetriever(client=unshadow, k=8, prefer_context=True)
77
+ docs = retriever.invoke("What did we decide about pnpm?")
78
+ ```
79
+
80
+ `prefer_context` uses `POST /context` and splits `[mem:uuid]` lines. This is not `ConversationBufferMemory`.
81
+
82
+ ### LlamaIndex
83
+
84
+ ```bash
85
+ pip install "./packages/unshadow[llamaindex]"
86
+ ```
87
+
88
+ ```python
89
+ from llama_index_unshadow import UnshadowRetriever
90
+ from unshadow import Unshadow
91
+
92
+ unshadow = Unshadow(api_key="unshadow_…")
93
+ retriever = UnshadowRetriever(unshadow, k=8, prefer_context=True)
94
+ nodes = retriever.retrieve("What did we decide about pnpm?")
95
+ ```
96
+
97
+ ## Agent bank
98
+
99
+ `UnshadowBank` is retain / recall / authorize. Those calls still use `/lattice/*`.
100
+
101
+ ```python
102
+ from unshadow import UnshadowBank
103
+
104
+ bank = UnshadowBank(api_key="unshadow_…")
105
+ bank.remember("Ship on Fridays is forbidden.", conversation_key="agent-main")
106
+ packed = bank.context("release policy")
107
+ receipt = bank.authorize(["memory-id"], query="deploy production")
108
+ bank.explain(packed["operation_id"])
109
+ ```
110
+
111
+ | Name | HTTP |
112
+ |------|------|
113
+ | `remember` | `POST /lattice/retain` (delta, 32k cap, 409 retry) |
114
+ | `explain` | `POST /lattice/explain` |
115
+ | `authorize` | `POST /lattice/recall` with `purpose: tool_arg` |
116
+
117
+ `retain` is an alias of `remember`. Hermes still uses `packages/lattice-hermes`.
118
+
119
+ The bank client does not ingest files. Capture stores pages and repo notes. Link that project into the agent bank. `remember(..., source_url=...)` cites a URL.
120
+
121
+ ## Tests
122
+
123
+ ```bash
124
+ python -m unittest discover -s packages/unshadow/tests
125
+ ```
@@ -0,0 +1,14 @@
1
+ langchain_unshadow/__init__.py,sha256=M2nHwmCXyCuPV_LZVSZW12bB-6mDlUd0toes4z6pdDQ,174
2
+ langchain_unshadow/retriever.py,sha256=tI79_1b4wTop6xDsDicNcNM7P1sre2n1dsD3VgTTiNk,1153
3
+ llama_index_unshadow/__init__.py,sha256=2vVEHSSNk9BisWwfM4-EUnLh52D-9_0Lm3j1bbwqFa8,179
4
+ llama_index_unshadow/retriever.py,sha256=Xaw4u4-mfPqlZ4F2q_zKIByO64Iq-8p4uYNpi9avNjE,1316
5
+ unshadow/__init__.py,sha256=qByuz4bf1KuJpkZzyy9J-l1LcgyPj2iIP-k_ODPyCsE,382
6
+ unshadow/bank.py,sha256=ZE93Eh2P4m6Xa75z037bEM65ySrG-ztmVHDxHTEeiK0,4157
7
+ unshadow/client.py,sha256=QrojShFsZEskbBhH09iXrkBOKp1gYHzexjL3bYAkW_w,3173
8
+ unshadow/documents.py,sha256=P9m7N8ttaSzMzkTawvkRlLCK4mUzRoF9wm_YYbO46Dw,2647
9
+ unshadow/http.py,sha256=P9SATKIDV4fFEZfYzshrwR7RCs3oJR2vDTeAjmMfOnM,3185
10
+ unshadow-0.2.0.dist-info/licenses/LICENSE,sha256=FxDTXMJvCxG3ZRqVsczpBrEoDmru0yog3fcZYVtimj8,1065
11
+ unshadow-0.2.0.dist-info/METADATA,sha256=iJh1chjYAqyAWPYqZiuCefqjUs8WoaRk9mq0Ic6I6EU,4060
12
+ unshadow-0.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
13
+ unshadow-0.2.0.dist-info/top_level.txt,sha256=q67oJFmQXHOr3f1xgpVtHkP2ZqXvgYI389Nw8lhW6qc,49
14
+ unshadow-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Unshadow
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ langchain_unshadow
2
+ llama_index_unshadow
3
+ unshadow