warm-sdk 0.1.3__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.
Files changed (94) hide show
  1. warm_sdk-0.1.3/.gitignore +85 -0
  2. warm_sdk-0.1.3/PKG-INFO +207 -0
  3. warm_sdk-0.1.3/README.md +115 -0
  4. warm_sdk-0.1.3/benchmarks/client_overhead.py +233 -0
  5. warm_sdk-0.1.3/pyproject.toml +118 -0
  6. warm_sdk-0.1.3/tests/importers/__init__.py +0 -0
  7. warm_sdk-0.1.3/tests/importers/conftest.py +143 -0
  8. warm_sdk-0.1.3/tests/importers/helpers_embedder.py +22 -0
  9. warm_sdk-0.1.3/tests/importers/mockserver.py +161 -0
  10. warm_sdk-0.1.3/tests/importers/test_cloud_b2.py +588 -0
  11. warm_sdk-0.1.3/tests/importers/test_core.py +282 -0
  12. warm_sdk-0.1.3/tests/importers/test_files.py +19 -0
  13. warm_sdk-0.1.3/tests/importers/test_identity.py +294 -0
  14. warm_sdk-0.1.3/tests/importers/test_indexes_a2.py +329 -0
  15. warm_sdk-0.1.3/tests/importers/test_markdown.py +86 -0
  16. warm_sdk-0.1.3/tests/importers/test_memory_c1.py +591 -0
  17. warm_sdk-0.1.3/tests/importers/test_openai_vs.py +127 -0
  18. warm_sdk-0.1.3/tests/importers/test_servers_b1.py +395 -0
  19. warm_sdk-0.1.3/tests/importers/test_sink_retries.py +52 -0
  20. warm_sdk-0.1.3/tests/importers/test_stores_c2.py +151 -0
  21. warm_sdk-0.1.3/tests/importers/test_vectordbs_a1.py +329 -0
  22. warm_sdk-0.1.3/tests/test_client.py +198 -0
  23. warm_sdk-0.1.3/tests/test_evidence.py +227 -0
  24. warm_sdk-0.1.3/tests/test_importer_metadata_validation.py +38 -0
  25. warm_sdk-0.1.3/tests/test_live_server.py +164 -0
  26. warm_sdk-0.1.3/tests/test_memory.py +260 -0
  27. warm_sdk-0.1.3/tests/test_transport.py +100 -0
  28. warm_sdk-0.1.3/uv.lock +15355 -0
  29. warm_sdk-0.1.3/writerslogic_warm_sdk/__init__.py +21 -0
  30. warm_sdk-0.1.3/writerslogic_warm_sdk/_transport.py +118 -0
  31. warm_sdk-0.1.3/writerslogic_warm_sdk/client.py +281 -0
  32. warm_sdk-0.1.3/writerslogic_warm_sdk/crewai/__init__.py +5 -0
  33. warm_sdk-0.1.3/writerslogic_warm_sdk/crewai/tools.py +44 -0
  34. warm_sdk-0.1.3/writerslogic_warm_sdk/dspy/__init__.py +5 -0
  35. warm_sdk-0.1.3/writerslogic_warm_sdk/dspy/retriever.py +43 -0
  36. warm_sdk-0.1.3/writerslogic_warm_sdk/embedchain/__init__.py +5 -0
  37. warm_sdk-0.1.3/writerslogic_warm_sdk/embedchain/vectordb.py +59 -0
  38. warm_sdk-0.1.3/writerslogic_warm_sdk/evidence.py +225 -0
  39. warm_sdk-0.1.3/writerslogic_warm_sdk/haystack/__init__.py +5 -0
  40. warm_sdk-0.1.3/writerslogic_warm_sdk/haystack/document_store.py +44 -0
  41. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/__init__.py +38 -0
  42. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/checkpoint.py +96 -0
  43. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/cli.py +189 -0
  44. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/common.py +214 -0
  45. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/inventory.py +103 -0
  46. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/mapping.py +364 -0
  47. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/model.py +79 -0
  48. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/options.py +114 -0
  49. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/pipeline.py +355 -0
  50. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/report.py +182 -0
  51. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/sink.py +254 -0
  52. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/sources/__init__.py +61 -0
  53. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/sources/_fields.py +117 -0
  54. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/sources/_sidecar.py +108 -0
  55. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/sources/annoy.py +102 -0
  56. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/sources/base.py +169 -0
  57. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/sources/chroma.py +122 -0
  58. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/sources/elastic.py +276 -0
  59. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/sources/faiss.py +153 -0
  60. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/sources/files.py +338 -0
  61. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/sources/hnswlib.py +127 -0
  62. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/sources/lancedb.py +117 -0
  63. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/sources/langchain.py +207 -0
  64. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/sources/letta.py +148 -0
  65. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/sources/llamaindex.py +128 -0
  66. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/sources/markdown.py +202 -0
  67. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/sources/mem0.py +303 -0
  68. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/sources/milvus.py +191 -0
  69. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/sources/mongodb.py +162 -0
  70. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/sources/openai_vs.py +110 -0
  71. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/sources/pgvector.py +140 -0
  72. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/sources/pinecone.py +181 -0
  73. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/sources/qdrant.py +170 -0
  74. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/sources/redis.py +223 -0
  75. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/sources/sqlite_vec.py +149 -0
  76. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/sources/vespa.py +233 -0
  77. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/sources/weaviate.py +186 -0
  78. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/sources/zep.py +144 -0
  79. warm_sdk-0.1.3/writerslogic_warm_sdk/importers/verify.py +257 -0
  80. warm_sdk-0.1.3/writerslogic_warm_sdk/langchain/__init__.py +5 -0
  81. warm_sdk-0.1.3/writerslogic_warm_sdk/langchain/vectorstores.py +65 -0
  82. warm_sdk-0.1.3/writerslogic_warm_sdk/llamaindex/__init__.py +5 -0
  83. warm_sdk-0.1.3/writerslogic_warm_sdk/llamaindex/vectorstores.py +68 -0
  84. warm_sdk-0.1.3/writerslogic_warm_sdk/memory.py +756 -0
  85. warm_sdk-0.1.3/writerslogic_warm_sdk/memory.pyi +139 -0
  86. warm_sdk-0.1.3/writerslogic_warm_sdk/phidata/__init__.py +5 -0
  87. warm_sdk-0.1.3/writerslogic_warm_sdk/phidata/vectordb.py +61 -0
  88. warm_sdk-0.1.3/writerslogic_warm_sdk/py.typed +0 -0
  89. warm_sdk-0.1.3/writerslogic_warm_sdk/pydantic_ai/__init__.py +7 -0
  90. warm_sdk-0.1.3/writerslogic_warm_sdk/pydantic_ai/tools.py +39 -0
  91. warm_sdk-0.1.3/writerslogic_warm_sdk/semantic_kernel/__init__.py +2 -0
  92. warm_sdk-0.1.3/writerslogic_warm_sdk/semantic_kernel/memory_store.py +95 -0
  93. warm_sdk-0.1.3/writerslogic_warm_sdk/smolagents/__init__.py +5 -0
  94. warm_sdk-0.1.3/writerslogic_warm_sdk/smolagents/tools.py +51 -0
@@ -0,0 +1,85 @@
1
+ # Rust
2
+ /target
3
+ /fuzz/Cargo.lock
4
+ /bindings/wasm/Cargo.lock
5
+ /fuzz/target
6
+ /fuzz/corpus
7
+ /fuzz/artifacts
8
+
9
+ # Node.js
10
+ /node_modules
11
+ package-lock.json
12
+ !interop/w3c/package-lock.json
13
+ !integrations/mcp/tests/package-lock.json
14
+ !integrations/js/package-lock.json
15
+ interop/w3c/node_modules/
16
+ *.node
17
+ !index.js
18
+
19
+ # OS
20
+ .DS_Store
21
+ Thumbs.db
22
+
23
+ # Databases (Local dev)
24
+ memory_db/
25
+ *.db
26
+ *.redb
27
+ vectors_data.bin/
28
+
29
+ # Environment / Private
30
+ .cargo_home/
31
+ .env
32
+ .env.*
33
+ !.env.example
34
+ !.env.sample
35
+
36
+ # Python
37
+ __pycache__/
38
+ .venv/
39
+
40
+ # uv script lockfiles written beside the benchmark scripts
41
+ benchmarks/public/*.py.lock
42
+
43
+ # A build driver that joins an absolute path under the cwd recreates this tree at the repo root
44
+ /Volumes/
45
+
46
+ # Evidence kept off git: benchmarks/evidence/MANIFEST.json lists the release archives and
47
+ # scripts/fetch_evidence.py restores them to these paths.
48
+ /benchmarks/results/superiority_v2/
49
+ /benchmarks/results/superiority_v3/
50
+ /benchmarks/results/native_harness_v1/
51
+ /benchmarks/results/native_harness_gce_v1/
52
+ /benchmarks/results/ann_extended_diagnostic_v1/
53
+ /benchmarks/results/ann_minimal_diagnostic_v1/
54
+ /benchmarks/results/algebraic_codebook_synthetic.json
55
+ /benchmarks/results/longmemeval_dev_answers_v2.json
56
+ /benchmarks/results/longmemeval_dev_answers_v2b.json
57
+
58
+ # Golden stores are committed test data.
59
+ !tests/golden/**/vectors_data.bin/
60
+
61
+ # Documentation build
62
+ /.docs-gen/
63
+ /scripts/docs-api/node_modules/
64
+ /scripts/docs-qa/node_modules/
65
+ /_site/
66
+ !/scripts/docs-qa/package-lock.json
67
+ !/scripts/docs-api/package-lock.json
68
+ # Local embedder model packs are build products (see bindings/embed-models)
69
+ *.hmsemb
70
+ bindings/embed-models/**/packs/
71
+ .warm-local/
72
+ benchmarks/results/research/ann/*.graph
73
+
74
+ # Historical benchmark inputs are restored locally from benchmarks/evidence/retired-inputs.json.
75
+ /benchmarks/results/metal_sdpa_draft_2026-10-09/isolated/
76
+ /benchmarks/results/metal_sdpa_draft_2026-10-09/dependencies/
77
+ /benchmarks/results/query_aware_graph_v1/sources/
78
+ /benchmarks/results/query_aware_graph_v4/runs/llama/snapshots/
79
+ /benchmarks/results/query_aware_graph_v4/runs/yi/snapshots/
80
+ /benchmarks/results/longmemeval_dev_answers_v1.json
81
+ /benchmarks/results/longmemeval_dev_answers_v3.json
82
+ /benchmarks/results/longmemeval_dev_answers_local30b_v1.json
83
+
84
+ /_legacy-docs/
85
+ /.hms-local/
@@ -0,0 +1,207 @@
1
+ Metadata-Version: 2.5
2
+ Name: warm-sdk
3
+ Version: 0.1.3
4
+ Summary: HTTP client and agent-framework adapters for a WritersLogic Adaptive Retention & Memory service
5
+ Project-URL: Homepage, https://writerslogic.com/warm/
6
+ Project-URL: Documentation, https://writerslogic.com/warm/docs/
7
+ Project-URL: Repository, https://github.com/writerslogic/warm
8
+ Author-email: WritersLogic <david@writerslogic.com>
9
+ License: Apache-2.0
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: httpx>=0.27.0
12
+ Requires-Dist: sniffio>=1.3
13
+ Provides-Extra: all
14
+ Requires-Dist: crewai>=0.28.0; extra == 'all'
15
+ Requires-Dist: dspy-ai>=2.4.0; extra == 'all'
16
+ Requires-Dist: embedchain>=0.1.114; extra == 'all'
17
+ Requires-Dist: haystack-ai>=2.0.0; extra == 'all'
18
+ Requires-Dist: langchain-core>=0.1.0; extra == 'all'
19
+ Requires-Dist: llama-index-core>=0.10.0; extra == 'all'
20
+ Requires-Dist: phidata>=2.4.0; extra == 'all'
21
+ Requires-Dist: pydantic-ai>=0.0.14; extra == 'all'
22
+ Requires-Dist: semantic-kernel>=1.39.4; extra == 'all'
23
+ Requires-Dist: smolagents>=1.0.0; extra == 'all'
24
+ Provides-Extra: crewai
25
+ Requires-Dist: crewai>=0.28.0; extra == 'crewai'
26
+ Provides-Extra: dspy
27
+ Requires-Dist: dspy-ai>=2.4.0; extra == 'dspy'
28
+ Provides-Extra: embedchain
29
+ Requires-Dist: embedchain>=0.1.114; extra == 'embedchain'
30
+ Provides-Extra: fast
31
+ Requires-Dist: orjson>=3.9; extra == 'fast'
32
+ Provides-Extra: haystack
33
+ Requires-Dist: haystack-ai>=2.0.0; extra == 'haystack'
34
+ Provides-Extra: import-annoy
35
+ Requires-Dist: annoy>=1.17; extra == 'import-annoy'
36
+ Requires-Dist: numpy>=1.26; extra == 'import-annoy'
37
+ Provides-Extra: import-chroma
38
+ Requires-Dist: chromadb>=0.5; extra == 'import-chroma'
39
+ Provides-Extra: import-faiss
40
+ Requires-Dist: faiss-cpu>=1.8; extra == 'import-faiss'
41
+ Requires-Dist: numpy>=1.26; extra == 'import-faiss'
42
+ Provides-Extra: import-files
43
+ Requires-Dist: duckdb>=1.0; extra == 'import-files'
44
+ Requires-Dist: numpy>=1.26; extra == 'import-files'
45
+ Requires-Dist: pyarrow>=15; extra == 'import-files'
46
+ Provides-Extra: import-hnswlib
47
+ Requires-Dist: hnswlib>=0.8; extra == 'import-hnswlib'
48
+ Requires-Dist: numpy>=1.26; extra == 'import-hnswlib'
49
+ Provides-Extra: import-lancedb
50
+ Requires-Dist: lancedb>=0.9; extra == 'import-lancedb'
51
+ Requires-Dist: pyarrow>=15; extra == 'import-lancedb'
52
+ Provides-Extra: import-langchain
53
+ Requires-Dist: faiss-cpu>=1.8; extra == 'import-langchain'
54
+ Requires-Dist: langchain-core>=0.2; extra == 'import-langchain'
55
+ Provides-Extra: import-letta
56
+ Requires-Dist: letta-client>=0.1; extra == 'import-letta'
57
+ Provides-Extra: import-llamaindex
58
+ Requires-Dist: llama-index-core>=0.10; extra == 'import-llamaindex'
59
+ Provides-Extra: import-mem0
60
+ Requires-Dist: mem0ai<0.2,>=0.1; extra == 'import-mem0'
61
+ Provides-Extra: import-milvus
62
+ Requires-Dist: pymilvus>=2.4; extra == 'import-milvus'
63
+ Provides-Extra: import-mongodb
64
+ Requires-Dist: pymongo>=4.6; extra == 'import-mongodb'
65
+ Provides-Extra: import-openai
66
+ Requires-Dist: openai>=1.40; extra == 'import-openai'
67
+ Provides-Extra: import-pgvector
68
+ Requires-Dist: psycopg[binary]>=3.1; extra == 'import-pgvector'
69
+ Provides-Extra: import-pinecone
70
+ Requires-Dist: pinecone>=5; extra == 'import-pinecone'
71
+ Provides-Extra: import-qdrant
72
+ Requires-Dist: qdrant-client>=1.9; extra == 'import-qdrant'
73
+ Provides-Extra: import-redis
74
+ Requires-Dist: redis>=5; extra == 'import-redis'
75
+ Provides-Extra: import-sqlite-vec
76
+ Requires-Dist: sqlite-vec>=0.1.1; extra == 'import-sqlite-vec'
77
+ Provides-Extra: import-zep
78
+ Requires-Dist: zep-cloud>=2; extra == 'import-zep'
79
+ Provides-Extra: langchain
80
+ Requires-Dist: langchain-core>=0.1.0; extra == 'langchain'
81
+ Provides-Extra: llamaindex
82
+ Requires-Dist: llama-index-core>=0.10.0; extra == 'llamaindex'
83
+ Provides-Extra: phidata
84
+ Requires-Dist: phidata>=2.4.0; extra == 'phidata'
85
+ Provides-Extra: pydantic-ai
86
+ Requires-Dist: pydantic-ai>=0.0.14; extra == 'pydantic-ai'
87
+ Provides-Extra: semantic-kernel
88
+ Requires-Dist: semantic-kernel>=1.39.4; extra == 'semantic-kernel'
89
+ Provides-Extra: smolagents
90
+ Requires-Dist: smolagents>=1.0.0; extra == 'smolagents'
91
+ Description-Content-Type: text/markdown
92
+
93
+ <a href="https://writerslogic.com/warm/"><img src="https://raw.githubusercontent.com/writerslogic/warm/main/assets/crystal-halo/crystal-detailed-static.png" width="96" alt="WARM logo" align="left"></a>
94
+
95
+ # WARM SDK
96
+
97
+ Python HTTP client and framework adapters for a WARM service.
98
+
99
+ [Website](https://writerslogic.com/warm/) · [Documentation](https://writerslogic.com/warm/docs/) · [Source](https://github.com/writerslogic/warm) · [License](https://github.com/writerslogic/warm/blob/main/LICENSE)
100
+
101
+ <br clear="left">
102
+
103
+ The client talks to `warm-server`, which ships in the WARM repository. The server
104
+ stores vectors you supply and exposes document and query endpoints. See the
105
+ [server guide](https://writerslogic.com/warm/docs/latest/SERVER/) to run it.
106
+
107
+ ## Installation
108
+
109
+ ```bash
110
+ uv add warm-sdk
111
+ ```
112
+
113
+ Framework adapters are optional extras. Install one, or all of them:
114
+
115
+ ```bash
116
+ uv add "warm-sdk[langchain]"
117
+ uv add "warm-sdk[all]"
118
+ ```
119
+
120
+ The earlier standalone `langchain-holographic` distribution is retired. For LangChain, install
121
+ `warm-sdk[langchain]` and import `WarmVectorStore` from
122
+ `writerslogic_warm_sdk.langchain`. This adapter connects to `warm-server`; update the old
123
+ `langchain_holographic` import and its local-store configuration when migrating.
124
+
125
+ The `fast` extra adds `orjson` for request encoding, which cuts the per-query cost of sending an embedding:
126
+
127
+ ```bash
128
+ uv add "warm-sdk[fast]"
129
+ ```
130
+
131
+ Requires Python 3.10 or newer.
132
+
133
+ ## Quick Start
134
+
135
+ ```python
136
+ from writerslogic_warm_sdk import WarmClient
137
+
138
+ client = WarmClient(url="http://localhost:8080", api_key="...")
139
+
140
+ client.add_documents([
141
+ {"id": "doc-1", "text": "Paris is the capital of France.", "vector": [0.12, -0.03, 0.88], "metadata": {"lang": "en"}},
142
+ ])
143
+
144
+ response = client.query(query_vector=[0.10, -0.01, 0.90], top_k=3, filter={"lang": "en"})
145
+ print(response["matches"])
146
+ client.delete_document("doc-1")
147
+ client.close()
148
+ ```
149
+
150
+ `AsyncWarmClient` has the same methods as coroutines; close it with `await client.aclose()`. Embeddings come from your own model: the SDK sends vectors, it does not compute them.
151
+
152
+ After bulk ingestion, `client.maintain_document_index()` explicitly builds the server's
153
+ exact dense document index and returns `{"documentIndexTrained": true}` when the
154
+ store is eligible. Smaller or unsupported stores return `false` and use exact scanning.
155
+ Use `await client.maintain_document_index()` with the async client. Rebuild after updates
156
+ or restart; the index is a runtime cache. Searches preserve the existing cosine results,
157
+ and metadata filters continue to apply before ranking. This method requires an
158
+ `warm-server` version exposing `POST /api/v1/index/maintain`.
159
+
160
+ ## Source Evidence
161
+
162
+ `complete_evidence` packs complete source observations in your retrieval order. Each observation contains a turn handle, session handle, observation time, role, and verbatim text. Supply opaque caller IDs as lowercase SHA-256 hex handles, and keep their association with your original records.
163
+
164
+ ```python
165
+ from hashlib import sha256
166
+ from writerslogic_warm_sdk import complete_evidence, decode_source_packet
167
+
168
+ def handle(identity):
169
+ return sha256(identity.encode("utf-8")).hexdigest()
170
+
171
+ session = handle("session-7")
172
+ candidates = [
173
+ [(handle("turn-1"), session, "2026-10-13T10:00:00Z", "user",
174
+ "I have two aquarium tanks.")],
175
+ [(handle("turn-2"), session, "2026-10-13T10:00:00Z", "assistant",
176
+ "You mentioned two aquarium tanks.")],
177
+ ]
178
+
179
+ selection = complete_evidence(candidates, selected=[0], budget_bytes=2048)
180
+ observations = decode_source_packet(selection.packet)
181
+ assert observations == [candidates[index] for index in selection.selected]
182
+ ```
183
+
184
+ The indices in `selected` are mandatory; packing raises `ValueError` if they exceed the budget. Additional complete groups fit in candidate order, with shared text and session metadata stored once. Decoding preserves every occurrence, role, observation time, and source text. The budget counts UTF-8 packet bytes; measure reader tokens separately. Packets contain source observations and no inferred answer or event date. These helpers run locally and make no service calls.
185
+
186
+ ## Integrations
187
+
188
+ | Framework | Extra | Import |
189
+ |-----------|-------|--------|
190
+ | LangChain | `langchain` | `from writerslogic_warm_sdk.langchain import WarmVectorStore` |
191
+ | LlamaIndex | `llamaindex` | `from writerslogic_warm_sdk.llamaindex import WarmVectorStore` |
192
+ | Haystack | `haystack` | `from writerslogic_warm_sdk.haystack import WarmDocumentStore` |
193
+ | Semantic Kernel | `semantic-kernel` | `from writerslogic_warm_sdk.semantic_kernel import WarmMemoryStore` |
194
+ | DSPy | `dspy` | `from writerslogic_warm_sdk.dspy import WarmRM` |
195
+ | CrewAI | `crewai` | `from writerslogic_warm_sdk.crewai import WarmSearchTool` |
196
+ | Embedchain | `embedchain` | `from writerslogic_warm_sdk.embedchain import WarmDB` |
197
+ | Phidata | `phidata` | `from writerslogic_warm_sdk.phidata import WarmVectorDb` |
198
+ | Pydantic AI | `pydantic-ai` | `from writerslogic_warm_sdk.pydantic_ai import get_warm_tool` |
199
+ | SmolAgents | `smolagents` | `from writerslogic_warm_sdk.smolagents import WarmSearchTool` |
200
+
201
+ ## Vector Masking
202
+
203
+ Passing `zero_trust_key` masks every embedding client-side with a keyed signed permutation (`VectorMask`, scrypt-derived). The server can still rank vectors because inner products and cosine similarity are preserved exactly. This is obfuscation, **not encryption**: the server learns all pairwise similarities, and known plain/masked pairs reveal the key for the coordinates they cover. Pass a per-collection `mask_salt` (at least 16 bytes) shared by all clients of that collection. Masks produced by 0.1.0/0.1.1 are not compatible and were not secure.
204
+
205
+ ## License
206
+
207
+ Apache-2.0. See the [LICENSE](https://github.com/writerslogic/warm/blob/main/LICENSE).
@@ -0,0 +1,115 @@
1
+ <a href="https://writerslogic.com/warm/"><img src="https://raw.githubusercontent.com/writerslogic/warm/main/assets/crystal-halo/crystal-detailed-static.png" width="96" alt="WARM logo" align="left"></a>
2
+
3
+ # WARM SDK
4
+
5
+ Python HTTP client and framework adapters for a WARM service.
6
+
7
+ [Website](https://writerslogic.com/warm/) · [Documentation](https://writerslogic.com/warm/docs/) · [Source](https://github.com/writerslogic/warm) · [License](https://github.com/writerslogic/warm/blob/main/LICENSE)
8
+
9
+ <br clear="left">
10
+
11
+ The client talks to `warm-server`, which ships in the WARM repository. The server
12
+ stores vectors you supply and exposes document and query endpoints. See the
13
+ [server guide](https://writerslogic.com/warm/docs/latest/SERVER/) to run it.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ uv add warm-sdk
19
+ ```
20
+
21
+ Framework adapters are optional extras. Install one, or all of them:
22
+
23
+ ```bash
24
+ uv add "warm-sdk[langchain]"
25
+ uv add "warm-sdk[all]"
26
+ ```
27
+
28
+ The earlier standalone `langchain-holographic` distribution is retired. For LangChain, install
29
+ `warm-sdk[langchain]` and import `WarmVectorStore` from
30
+ `writerslogic_warm_sdk.langchain`. This adapter connects to `warm-server`; update the old
31
+ `langchain_holographic` import and its local-store configuration when migrating.
32
+
33
+ The `fast` extra adds `orjson` for request encoding, which cuts the per-query cost of sending an embedding:
34
+
35
+ ```bash
36
+ uv add "warm-sdk[fast]"
37
+ ```
38
+
39
+ Requires Python 3.10 or newer.
40
+
41
+ ## Quick Start
42
+
43
+ ```python
44
+ from writerslogic_warm_sdk import WarmClient
45
+
46
+ client = WarmClient(url="http://localhost:8080", api_key="...")
47
+
48
+ client.add_documents([
49
+ {"id": "doc-1", "text": "Paris is the capital of France.", "vector": [0.12, -0.03, 0.88], "metadata": {"lang": "en"}},
50
+ ])
51
+
52
+ response = client.query(query_vector=[0.10, -0.01, 0.90], top_k=3, filter={"lang": "en"})
53
+ print(response["matches"])
54
+ client.delete_document("doc-1")
55
+ client.close()
56
+ ```
57
+
58
+ `AsyncWarmClient` has the same methods as coroutines; close it with `await client.aclose()`. Embeddings come from your own model: the SDK sends vectors, it does not compute them.
59
+
60
+ After bulk ingestion, `client.maintain_document_index()` explicitly builds the server's
61
+ exact dense document index and returns `{"documentIndexTrained": true}` when the
62
+ store is eligible. Smaller or unsupported stores return `false` and use exact scanning.
63
+ Use `await client.maintain_document_index()` with the async client. Rebuild after updates
64
+ or restart; the index is a runtime cache. Searches preserve the existing cosine results,
65
+ and metadata filters continue to apply before ranking. This method requires an
66
+ `warm-server` version exposing `POST /api/v1/index/maintain`.
67
+
68
+ ## Source Evidence
69
+
70
+ `complete_evidence` packs complete source observations in your retrieval order. Each observation contains a turn handle, session handle, observation time, role, and verbatim text. Supply opaque caller IDs as lowercase SHA-256 hex handles, and keep their association with your original records.
71
+
72
+ ```python
73
+ from hashlib import sha256
74
+ from writerslogic_warm_sdk import complete_evidence, decode_source_packet
75
+
76
+ def handle(identity):
77
+ return sha256(identity.encode("utf-8")).hexdigest()
78
+
79
+ session = handle("session-7")
80
+ candidates = [
81
+ [(handle("turn-1"), session, "2026-10-13T10:00:00Z", "user",
82
+ "I have two aquarium tanks.")],
83
+ [(handle("turn-2"), session, "2026-10-13T10:00:00Z", "assistant",
84
+ "You mentioned two aquarium tanks.")],
85
+ ]
86
+
87
+ selection = complete_evidence(candidates, selected=[0], budget_bytes=2048)
88
+ observations = decode_source_packet(selection.packet)
89
+ assert observations == [candidates[index] for index in selection.selected]
90
+ ```
91
+
92
+ The indices in `selected` are mandatory; packing raises `ValueError` if they exceed the budget. Additional complete groups fit in candidate order, with shared text and session metadata stored once. Decoding preserves every occurrence, role, observation time, and source text. The budget counts UTF-8 packet bytes; measure reader tokens separately. Packets contain source observations and no inferred answer or event date. These helpers run locally and make no service calls.
93
+
94
+ ## Integrations
95
+
96
+ | Framework | Extra | Import |
97
+ |-----------|-------|--------|
98
+ | LangChain | `langchain` | `from writerslogic_warm_sdk.langchain import WarmVectorStore` |
99
+ | LlamaIndex | `llamaindex` | `from writerslogic_warm_sdk.llamaindex import WarmVectorStore` |
100
+ | Haystack | `haystack` | `from writerslogic_warm_sdk.haystack import WarmDocumentStore` |
101
+ | Semantic Kernel | `semantic-kernel` | `from writerslogic_warm_sdk.semantic_kernel import WarmMemoryStore` |
102
+ | DSPy | `dspy` | `from writerslogic_warm_sdk.dspy import WarmRM` |
103
+ | CrewAI | `crewai` | `from writerslogic_warm_sdk.crewai import WarmSearchTool` |
104
+ | Embedchain | `embedchain` | `from writerslogic_warm_sdk.embedchain import WarmDB` |
105
+ | Phidata | `phidata` | `from writerslogic_warm_sdk.phidata import WarmVectorDb` |
106
+ | Pydantic AI | `pydantic-ai` | `from writerslogic_warm_sdk.pydantic_ai import get_warm_tool` |
107
+ | SmolAgents | `smolagents` | `from writerslogic_warm_sdk.smolagents import WarmSearchTool` |
108
+
109
+ ## Vector Masking
110
+
111
+ Passing `zero_trust_key` masks every embedding client-side with a keyed signed permutation (`VectorMask`, scrypt-derived). The server can still rank vectors because inner products and cosine similarity are preserved exactly. This is obfuscation, **not encryption**: the server learns all pairwise similarities, and known plain/masked pairs reveal the key for the coordinates they cover. Pass a per-collection `mask_salt` (at least 16 bytes) shared by all clients of that collection. Masks produced by 0.1.0/0.1.1 are not compatible and were not secure.
112
+
113
+ ## License
114
+
115
+ Apache-2.0. See the [LICENSE](https://github.com/writerslogic/warm/blob/main/LICENSE).
@@ -0,0 +1,233 @@
1
+ """Per-call client overhead of the SDK against a local warm-server, paired with raw http.client.
2
+
3
+ Starts the server binary given by ``--server``, loads ``--rows`` documents, then runs
4
+ ``--rounds`` interleaved rounds. Each round times every variant on query, add, replace and
5
+ delete, rotating the variant order between rounds. Variants:
6
+
7
+ - ``raw``: one persistent ``http.client.HTTPConnection`` with a pre-encoded JSON body.
8
+ - ``sdk``: ``WarmClient`` from the installed package (sync).
9
+ - ``sdk-async``: ``AsyncWarmClient`` awaited sequentially in one event loop.
10
+
11
+ The SDK is whichever ``writerslogic_warm_sdk`` is importable, so a before/after pair runs this script
12
+ once per SDK build (each in its own environment), interleaving the processes.
13
+
14
+ Writes a JSON summary with p50/p99 per variant and operation to ``--out``.
15
+ """
16
+
17
+ import argparse
18
+ import asyncio
19
+ import http.client
20
+ import importlib.metadata
21
+ import json
22
+ import os
23
+ import platform
24
+ import random
25
+ import statistics
26
+ import subprocess
27
+ import sys
28
+ import tempfile
29
+ import time
30
+
31
+ API_KEY = "bench-key"
32
+
33
+
34
+ def vec(rng, dim):
35
+ return [rng.uniform(-1.0, 1.0) for _ in range(dim)]
36
+
37
+
38
+ def pct(samples, q):
39
+ s = sorted(samples)
40
+ return s[min(len(s) - 1, int(q * len(s)))]
41
+
42
+
43
+ class Raw:
44
+ def __init__(self, url):
45
+ host, port = url.removeprefix("http://").split(":")
46
+ self.conn = http.client.HTTPConnection(host, int(port))
47
+ self.headers = {"Content-Type": "application/json", "Authorization": f"Bearer {API_KEY}"}
48
+
49
+ def call(self, method, path, body=None):
50
+ self.conn.request(method, path, body=body, headers=self.headers)
51
+ res = self.conn.getresponse()
52
+ data = res.read()
53
+ if res.status >= 300:
54
+ raise RuntimeError(f"{res.status} {data[:200]!r}")
55
+ return data
56
+
57
+
58
+ def installed(name):
59
+ try:
60
+ return importlib.metadata.version(name)
61
+ except importlib.metadata.PackageNotFoundError:
62
+ return None
63
+
64
+
65
+ def make_ops(rng, dim, n, tag):
66
+ """Per-variant inputs: queries, fresh docs to add, the same ids to replace, then delete."""
67
+ queries = [vec(rng, dim) for _ in range(n["query"])]
68
+ ids = [f"{tag}-{i}" for i in range(n["write"])]
69
+ adds = [{"id": i, "text": i, "vector": vec(rng, dim)} for i in ids]
70
+ reps = [{"id": i, "text": i, "vector": vec(rng, dim)} for i in ids]
71
+ return queries, adds, reps, ids
72
+
73
+
74
+ def time_sync(variant, client, ops, top_k):
75
+ queries, adds, reps, ids = ops
76
+ out = {k: [] for k in ("query", "add", "replace", "delete")}
77
+ clock = time.perf_counter_ns
78
+ if variant == "raw":
79
+ for q in queries:
80
+ t = clock()
81
+ json.loads(client.call("POST", "/api/v1/query", json.dumps({"query_vector": q, "top_k": top_k})))
82
+ out["query"].append(clock() - t)
83
+ for key, docs in (("add", adds), ("replace", reps)):
84
+ for d in docs:
85
+ t = clock()
86
+ client.call("POST", "/api/v1/documents/batch", json.dumps([d]))
87
+ out[key].append(clock() - t)
88
+ for i in ids:
89
+ t = clock()
90
+ client.call("DELETE", f"/api/v1/documents/{i}")
91
+ out["delete"].append(clock() - t)
92
+ return out
93
+ for q in queries:
94
+ t = clock()
95
+ client.query(q, top_k=top_k)
96
+ out["query"].append(clock() - t)
97
+ for key, docs in (("add", adds), ("replace", reps)):
98
+ for d in docs:
99
+ t = clock()
100
+ client.add_documents([d])
101
+ out[key].append(clock() - t)
102
+ for i in ids:
103
+ t = clock()
104
+ client.delete_document(i)
105
+ out["delete"].append(clock() - t)
106
+ return out
107
+
108
+
109
+ async def time_async(client, ops, top_k):
110
+ queries, adds, reps, ids = ops
111
+ out = {k: [] for k in ("query", "add", "replace", "delete")}
112
+ clock = time.perf_counter_ns
113
+ for q in queries:
114
+ t = clock()
115
+ await client.query(q, top_k=top_k)
116
+ out["query"].append(clock() - t)
117
+ for key, docs in (("add", adds), ("replace", reps)):
118
+ for d in docs:
119
+ t = clock()
120
+ await client.add_documents([d])
121
+ out[key].append(clock() - t)
122
+ for i in ids:
123
+ t = clock()
124
+ await client.delete_document(i)
125
+ out["delete"].append(clock() - t)
126
+ return out
127
+
128
+
129
+ def start_server(binary, dim, data_dir):
130
+ env = {**os.environ, "WARM_API_KEY": API_KEY}
131
+ proc = subprocess.Popen(
132
+ [binary, "--bind", "127.0.0.1:0", "--input-dim", str(dim), "--data-dir", data_dir],
133
+ stdout=subprocess.PIPE,
134
+ stderr=subprocess.DEVNULL,
135
+ text=True,
136
+ env=env,
137
+ )
138
+ line = proc.stdout.readline().strip()
139
+ if not line.startswith("warm-server listening on "):
140
+ proc.terminate()
141
+ raise RuntimeError(line)
142
+ return proc, f"http://{line.rsplit(' ', 1)[1]}"
143
+
144
+
145
+ def main():
146
+ ap = argparse.ArgumentParser()
147
+ ap.add_argument("--server", required=True)
148
+ ap.add_argument("--label", required=True)
149
+ ap.add_argument("--rows", type=int, default=10_000)
150
+ ap.add_argument("--dim", type=int, default=384)
151
+ ap.add_argument("--top-k", type=int, default=10)
152
+ ap.add_argument("--queries", type=int, default=2000)
153
+ ap.add_argument("--writes", type=int, default=500)
154
+ ap.add_argument("--rounds", type=int, default=3)
155
+ ap.add_argument("--out", required=True)
156
+ args = ap.parse_args()
157
+
158
+ from writerslogic_warm_sdk import AsyncWarmClient, WarmClient
159
+
160
+ impls = {"sdk": (WarmClient, AsyncWarmClient)}
161
+
162
+ rng = random.Random(7)
163
+ n = {"query": args.queries, "write": args.writes}
164
+ with tempfile.TemporaryDirectory() as data_dir:
165
+ proc, url = start_server(args.server, args.dim, data_dir)
166
+ try:
167
+ raw = Raw(url)
168
+ for start in range(0, args.rows, 1000):
169
+ docs = [{"id": f"row-{i}", "text": f"row {i}", "vector": vec(rng, args.dim)} for i in range(start, min(args.rows, start + 1000))]
170
+ raw.call("POST", "/api/v1/documents/batch", json.dumps(docs))
171
+ raw.call("POST", "/api/v1/index/maintain", "{}")
172
+
173
+ variants = ["raw"] + [v for name in impls for v in (name, f"{name}-async")]
174
+ samples = {v: {k: [] for k in ("query", "add", "replace", "delete")} for v in variants}
175
+ clients = {"raw": raw}
176
+ for name, (sync_cls, _) in impls.items():
177
+ clients[name] = sync_cls(url, api_key=API_KEY)
178
+ loop = asyncio.new_event_loop()
179
+ for name, (_, async_cls) in impls.items():
180
+ clients[f"{name}-async"] = async_cls(url, api_key=API_KEY)
181
+
182
+ # Warm every connection before the first timed call.
183
+ warm = make_ops(rng, args.dim, {"query": 50, "write": 20}, "warm")
184
+ for v in variants:
185
+ if v.endswith("-async"):
186
+ loop.run_until_complete(time_async(clients[v], warm, args.top_k))
187
+ else:
188
+ time_sync(v, clients[v], warm, args.top_k)
189
+ warm = make_ops(rng, args.dim, {"query": 50, "write": 20}, f"warm-{v}")
190
+
191
+ # Variants alternate in small blocks with a rotating order, so background load on a
192
+ # shared machine lands on raw and SDK calls alike and the per-call difference stays paired.
193
+ blocks = 20
194
+ block = {"query": max(1, n["query"] // blocks), "write": max(1, n["write"] // blocks)}
195
+ for r in range(args.rounds):
196
+ for b in range(blocks):
197
+ s = (r + b) % len(variants)
198
+ for v in variants[s:] + variants[:s]:
199
+ ops = make_ops(rng, args.dim, block, f"r{r}-b{b}-{v}")
200
+ if v.endswith("-async"):
201
+ got = loop.run_until_complete(time_async(clients[v], ops, args.top_k))
202
+ else:
203
+ got = time_sync(v, clients[v], ops, args.top_k)
204
+ for k, xs in got.items():
205
+ samples[v][k].extend(xs)
206
+ finally:
207
+ proc.terminate()
208
+ proc.wait(timeout=30)
209
+
210
+ summary = {}
211
+ for v, ops in samples.items():
212
+ summary[v] = {}
213
+ for k, xs in ops.items():
214
+ p50 = pct(xs, 0.5) / 1e3
215
+ p99 = pct(xs, 0.99) / 1e3
216
+ raw50 = pct(samples["raw"][k], 0.5) / 1e3
217
+ summary[v][k] = {"n": len(xs), "p50_us": round(p50, 1), "p99_us": round(p99, 1), "mean_us": round(statistics.fmean(xs) / 1e3, 1), "p50_over_raw_us": round(p50 - raw50, 1)}
218
+ result = {
219
+ "label": args.label,
220
+ "config": {k: v for k, v in vars(args).items() if k not in ("server", "out", "label")},
221
+ "python": sys.version.split()[0],
222
+ "packages": {p: installed(p) for p in ("httpx", "httpcore", "sniffio", "orjson")},
223
+ "machine": platform.machine(),
224
+ "load_average": os.getloadavg(),
225
+ "results": summary,
226
+ }
227
+ with open(args.out, "w") as f:
228
+ json.dump(result, f, indent=2)
229
+
230
+
231
+
232
+ if __name__ == "__main__":
233
+ main()