agno-moss 0.0.1__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,25 @@
1
+ BSD 2-Clause License
2
+
3
+ Copyright (c) 2025, InferEdge Inc.
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,105 @@
1
+ Metadata-Version: 2.4
2
+ Name: agno-moss
3
+ Version: 0.0.1
4
+ Summary: Moss semantic search integration for Agno agents
5
+ License: BSD-2-Clause
6
+ Requires-Python: <3.15,>=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: moss>=1.1.1
10
+ Requires-Dist: agno>=2.5.17
11
+ Dynamic: license-file
12
+
13
+ # agno-moss
14
+
15
+ The Moss in-memory semantic search runtime for [Agno](https://docs.agno.com) agents.
16
+
17
+ Moss manages embeddings internally and serves queries from an in-memory runtime — sub-10ms lookups, no external embedder, no vector database to run. Point `Knowledge` at `MossRuntime` and Agno agents get instant RAG with zero infrastructure.
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pip install agno-moss
23
+ # or
24
+ uv add agno-moss
25
+ ```
26
+
27
+ ## Prerequisites
28
+
29
+ - Moss project ID and project key — get them from the [Moss Portal](https://portal.usemoss.dev)
30
+ - Python 3.10+
31
+ - An Agno-compatible model provider (OpenAI, Anthropic, etc.)
32
+
33
+ ## Quickstart
34
+
35
+ ```python
36
+ import os
37
+ from agno.agent import Agent
38
+ from agno.knowledge.knowledge import Knowledge
39
+ from agno.models.anthropic import Claude
40
+ from agno_moss import MossRuntime
41
+
42
+ knowledge = Knowledge(
43
+ vector_db=MossRuntime(
44
+ index_name="my-index",
45
+ # Falls back to MOSS_PROJECT_ID / MOSS_PROJECT_KEY env vars
46
+ ),
47
+ )
48
+
49
+ agent = Agent(
50
+ model=Claude(id="claude-sonnet-4-20250514"),
51
+ knowledge=knowledge,
52
+ search_knowledge=True,
53
+ markdown=True,
54
+ )
55
+
56
+ knowledge.load(recreate=False)
57
+ agent.print_response("What do you know about our return policy?", stream=True)
58
+ ```
59
+
60
+ ## Configuration
61
+
62
+ ### MossRuntime
63
+
64
+ | Parameter | Default | Description |
65
+ |---|---|---|
66
+ | `index_name` | (required) | Name of the Moss index |
67
+ | `project_id` | `MOSS_PROJECT_ID` env var | Moss project ID |
68
+ | `project_key` | `MOSS_PROJECT_KEY` env var | Moss project key |
69
+ | `embedding_model` | `"moss-minilm"` | `"moss-minilm"` (fast) or `"moss-mediumlm"` (higher accuracy) |
70
+ | `alpha` | `0.8` | Hybrid search blend: 1.0 = semantic only, 0.0 = keyword only |
71
+ | `auto_refresh` | `False` | Auto-refresh the in-memory index when new docs are added |
72
+ | `polling_interval_in_seconds` | `600` | Refresh interval when `auto_refresh=True` |
73
+
74
+ ## How it works
75
+
76
+ `MossRuntime` implements Agno's `VectorDb` base class:
77
+
78
+ - **`create()`** — loads an existing index into Moss's in-memory runtime. Call once at startup for fast first queries.
79
+ - **`upsert()`** — creates the index on first call, then adds or updates documents. Loads the index automatically after each batch.
80
+ - **`search()`** — hybrid semantic + keyword search via the loaded in-memory runtime. Falls back to the cloud API if the index is not loaded.
81
+
82
+ Moss filters metadata **only when the index is loaded locally**. `content_hash_exists()` returns `False` when unloaded (safe: forces re-upsert rather than silently skipping).
83
+
84
+ ## Choosing a model provider
85
+
86
+ ```python
87
+ # OpenAI
88
+ from agno.models.openai import OpenAIChat
89
+ agent = Agent(model=OpenAIChat(id="gpt-4o"), knowledge=knowledge, search_knowledge=True)
90
+
91
+ # Anthropic
92
+ from agno.models.anthropic import Claude
93
+ agent = Agent(model=Claude(id="claude-sonnet-4-20250514"), knowledge=knowledge, search_knowledge=True)
94
+ ```
95
+
96
+ See the [Agno model providers docs](https://docs.agno.com/models/introduction) for the full list.
97
+
98
+ ## License
99
+
100
+ BSD 2-Clause — see [LICENSE](LICENSE).
101
+
102
+ ## Support
103
+
104
+ - [Moss Docs](https://docs.moss.dev)
105
+ - [Agno Docs](https://docs.agno.com)
@@ -0,0 +1,93 @@
1
+ # agno-moss
2
+
3
+ The Moss in-memory semantic search runtime for [Agno](https://docs.agno.com) agents.
4
+
5
+ Moss manages embeddings internally and serves queries from an in-memory runtime — sub-10ms lookups, no external embedder, no vector database to run. Point `Knowledge` at `MossRuntime` and Agno agents get instant RAG with zero infrastructure.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install agno-moss
11
+ # or
12
+ uv add agno-moss
13
+ ```
14
+
15
+ ## Prerequisites
16
+
17
+ - Moss project ID and project key — get them from the [Moss Portal](https://portal.usemoss.dev)
18
+ - Python 3.10+
19
+ - An Agno-compatible model provider (OpenAI, Anthropic, etc.)
20
+
21
+ ## Quickstart
22
+
23
+ ```python
24
+ import os
25
+ from agno.agent import Agent
26
+ from agno.knowledge.knowledge import Knowledge
27
+ from agno.models.anthropic import Claude
28
+ from agno_moss import MossRuntime
29
+
30
+ knowledge = Knowledge(
31
+ vector_db=MossRuntime(
32
+ index_name="my-index",
33
+ # Falls back to MOSS_PROJECT_ID / MOSS_PROJECT_KEY env vars
34
+ ),
35
+ )
36
+
37
+ agent = Agent(
38
+ model=Claude(id="claude-sonnet-4-20250514"),
39
+ knowledge=knowledge,
40
+ search_knowledge=True,
41
+ markdown=True,
42
+ )
43
+
44
+ knowledge.load(recreate=False)
45
+ agent.print_response("What do you know about our return policy?", stream=True)
46
+ ```
47
+
48
+ ## Configuration
49
+
50
+ ### MossRuntime
51
+
52
+ | Parameter | Default | Description |
53
+ |---|---|---|
54
+ | `index_name` | (required) | Name of the Moss index |
55
+ | `project_id` | `MOSS_PROJECT_ID` env var | Moss project ID |
56
+ | `project_key` | `MOSS_PROJECT_KEY` env var | Moss project key |
57
+ | `embedding_model` | `"moss-minilm"` | `"moss-minilm"` (fast) or `"moss-mediumlm"` (higher accuracy) |
58
+ | `alpha` | `0.8` | Hybrid search blend: 1.0 = semantic only, 0.0 = keyword only |
59
+ | `auto_refresh` | `False` | Auto-refresh the in-memory index when new docs are added |
60
+ | `polling_interval_in_seconds` | `600` | Refresh interval when `auto_refresh=True` |
61
+
62
+ ## How it works
63
+
64
+ `MossRuntime` implements Agno's `VectorDb` base class:
65
+
66
+ - **`create()`** — loads an existing index into Moss's in-memory runtime. Call once at startup for fast first queries.
67
+ - **`upsert()`** — creates the index on first call, then adds or updates documents. Loads the index automatically after each batch.
68
+ - **`search()`** — hybrid semantic + keyword search via the loaded in-memory runtime. Falls back to the cloud API if the index is not loaded.
69
+
70
+ Moss filters metadata **only when the index is loaded locally**. `content_hash_exists()` returns `False` when unloaded (safe: forces re-upsert rather than silently skipping).
71
+
72
+ ## Choosing a model provider
73
+
74
+ ```python
75
+ # OpenAI
76
+ from agno.models.openai import OpenAIChat
77
+ agent = Agent(model=OpenAIChat(id="gpt-4o"), knowledge=knowledge, search_knowledge=True)
78
+
79
+ # Anthropic
80
+ from agno.models.anthropic import Claude
81
+ agent = Agent(model=Claude(id="claude-sonnet-4-20250514"), knowledge=knowledge, search_knowledge=True)
82
+ ```
83
+
84
+ See the [Agno model providers docs](https://docs.agno.com/models/introduction) for the full list.
85
+
86
+ ## License
87
+
88
+ BSD 2-Clause — see [LICENSE](LICENSE).
89
+
90
+ ## Support
91
+
92
+ - [Moss Docs](https://docs.moss.dev)
93
+ - [Agno Docs](https://docs.agno.com)
@@ -0,0 +1,51 @@
1
+ [project]
2
+ name = "agno-moss"
3
+ version = "0.0.1"
4
+ description = "Moss semantic search integration for Agno agents"
5
+ readme = "README.md"
6
+ license = { text = "BSD-2-Clause" }
7
+ requires-python = ">=3.10,<3.15"
8
+ dependencies = [
9
+ "moss>=1.1.1",
10
+ "agno>=2.5.17",
11
+ ]
12
+
13
+ [dependency-groups]
14
+ dev = [
15
+ "python-dotenv>=1.2.1",
16
+ "ruff>=0.1.0",
17
+ "pytest>=8.0",
18
+ "pytest-asyncio>=0.23.0",
19
+ ]
20
+
21
+ [tool.ruff]
22
+ line-length = 100
23
+ target-version = "py310"
24
+
25
+ [tool.ruff.lint]
26
+ select = ["E", "W", "F", "I", "B", "UP", "D"]
27
+ ignore = ["D100", "D104"]
28
+
29
+ [tool.ruff.lint.per-file-ignores]
30
+ "tests/**/*.py" = ["D101", "D102", "D103", "D107"]
31
+ "examples/**/*.py" = ["D101", "D102", "D103"]
32
+
33
+ [tool.ruff.lint.pydocstyle]
34
+ convention = "google"
35
+
36
+ [tool.ruff.format]
37
+ quote-style = "double"
38
+ indent-style = "space"
39
+ skip-magic-trailing-comma = false
40
+ line-ending = "auto"
41
+
42
+ [tool.pytest.ini_options]
43
+ asyncio_mode = "auto"
44
+
45
+ [build-system]
46
+ requires = ["setuptools>=61.0"]
47
+ build-backend = "setuptools.build_meta"
48
+
49
+ [tool.setuptools.packages.find]
50
+ where = ["src"]
51
+ namespaces = false
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,22 @@
1
+ """Moss semantic search integration for Agno agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from moss import (
6
+ DocumentInfo,
7
+ GetDocumentsOptions,
8
+ IndexInfo,
9
+ MossClient,
10
+ SearchResult,
11
+ )
12
+
13
+ from .runtime import MossRuntime
14
+
15
+ __all__ = [
16
+ "DocumentInfo",
17
+ "GetDocumentsOptions",
18
+ "IndexInfo",
19
+ "MossClient",
20
+ "MossRuntime",
21
+ "SearchResult",
22
+ ]
@@ -0,0 +1,392 @@
1
+ """Agno integration for the Moss in-memory semantic search runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ import uuid
8
+ from concurrent.futures import ThreadPoolExecutor
9
+ from typing import Any
10
+
11
+ from agno.knowledge.document import Document
12
+ from agno.utils.log import log_debug, log_error, log_info, log_warning
13
+ from agno.vectordb.base import VectorDb
14
+ from moss import (
15
+ DocumentInfo,
16
+ GetDocumentsOptions,
17
+ MossClient,
18
+ MutationOptions,
19
+ QueryOptions,
20
+ )
21
+
22
+ __all__ = ["MossRuntime"]
23
+
24
+
25
+ class MossRuntime(VectorDb):
26
+ """Agno knowledge source backed by the Moss in-memory semantic search runtime.
27
+
28
+ Moss downloads your index and runs queries
29
+ entirely in-process, giving sub-10ms retrieval with no embedder and no
30
+ external infrastructure required.
31
+
32
+ Call ``create()`` once at startup to load an existing index; subsequent
33
+ ``search()`` calls run entirely in memory. On first ``upsert()``,
34
+ the index is created automatically if it does not exist.
35
+
36
+ Args:
37
+ index_name: Name of the Moss index (equivalent to a collection).
38
+ project_id: Moss project ID. Falls back to ``MOSS_PROJECT_ID`` env var.
39
+ project_key: Moss project key. Falls back to ``MOSS_PROJECT_KEY`` env var.
40
+ embedding_model: Moss embedding model — ``"moss-minilm"`` (default,
41
+ faster) or ``"moss-mediumlm"`` (higher accuracy).
42
+ alpha: Hybrid search weight — 1.0 = pure semantic, 0.0 = pure keyword.
43
+ Defaults to 0.8.
44
+ auto_refresh: Auto-refresh the loaded index when new docs are added.
45
+ polling_interval_in_seconds: Interval for auto-refresh. Defaults to 600.
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ index_name: str,
51
+ project_id: str | None = None,
52
+ project_key: str | None = None,
53
+ embedding_model: str = "moss-minilm",
54
+ alpha: float = 0.8,
55
+ auto_refresh: bool = False,
56
+ polling_interval_in_seconds: int = 600,
57
+ name: str | None = None,
58
+ description: str | None = None,
59
+ id: str | None = None,
60
+ ):
61
+ """Initialize the MossRuntime."""
62
+ self.index_name = index_name
63
+ self.embedding_model = embedding_model
64
+ self.alpha = alpha
65
+ self.auto_refresh = auto_refresh
66
+ self.polling_interval_in_seconds = polling_interval_in_seconds
67
+
68
+ resolved_id = project_id or os.getenv("MOSS_PROJECT_ID") or ""
69
+ resolved_key = project_key or os.getenv("MOSS_PROJECT_KEY") or ""
70
+
71
+ if not resolved_id or not resolved_key:
72
+ raise ValueError(
73
+ "Moss credentials required. Provide project_id and project_key "
74
+ "or set MOSS_PROJECT_ID and MOSS_PROJECT_KEY environment variables."
75
+ )
76
+
77
+ self._client: MossClient = MossClient(resolved_id, resolved_key)
78
+ self._index_loaded: bool = False
79
+
80
+ super().__init__(id=id, name=name or index_name, description=description)
81
+
82
+ # ------------------------------------------------------------------
83
+ # Internal helpers
84
+ # ------------------------------------------------------------------
85
+
86
+ def _run(self, coro: Any) -> Any:
87
+ """Run a coroutine from a sync context, even inside a running event loop.
88
+
89
+ asyncio.run() raises RuntimeError when called inside a running loop
90
+ (Jupyter, FastAPI async handlers). This detects that case and
91
+ dispatches to a fresh thread instead.
92
+ """
93
+ try:
94
+ asyncio.get_running_loop()
95
+ in_running_loop = True
96
+ except RuntimeError:
97
+ in_running_loop = False
98
+
99
+ if in_running_loop:
100
+ with ThreadPoolExecutor(max_workers=1) as pool:
101
+ return pool.submit(asyncio.run, coro).result()
102
+ return asyncio.run(coro)
103
+
104
+ def _to_moss_doc(self, document: Document, content_hash: str | None = None) -> DocumentInfo:
105
+ meta: dict[str, str] = {str(k): str(v) for k, v in (document.meta_data or {}).items()}
106
+ if content_hash:
107
+ meta["content_hash"] = content_hash
108
+ if document.content_id:
109
+ meta["content_id"] = document.content_id
110
+ if document.name:
111
+ meta["name"] = document.name
112
+ return DocumentInfo(
113
+ id=document.id or document.content_id or str(uuid.uuid4()),
114
+ text=document.content,
115
+ metadata=meta,
116
+ )
117
+
118
+ def _to_document(self, result: Any) -> Document:
119
+ meta = dict(result.metadata) if result.metadata else {}
120
+ if (score := getattr(result, "score", None)) is not None:
121
+ meta["_score"] = str(score)
122
+ return Document(
123
+ id=result.id,
124
+ content=result.text,
125
+ meta_data=meta,
126
+ name=meta.get("name"),
127
+ content_id=meta.get("content_id"),
128
+ )
129
+
130
+ async def _load_index(self) -> None:
131
+ if not self._index_loaded:
132
+ log_debug(f"Loading Moss index '{self.index_name}' into memory")
133
+ await self._client.load_index(
134
+ self.index_name,
135
+ auto_refresh=self.auto_refresh,
136
+ polling_interval_in_seconds=self.polling_interval_in_seconds,
137
+ )
138
+ self._index_loaded = True
139
+
140
+ async def _upsert_docs(self, moss_docs: list[DocumentInfo]) -> None:
141
+ if not await self.async_exists():
142
+ log_info(f"Creating Moss index '{self.index_name}' with model '{self.embedding_model}'")
143
+ await self._client.create_index(self.index_name, moss_docs, self.embedding_model)
144
+ else:
145
+ await self._client.add_docs(
146
+ self.index_name, moss_docs, options=MutationOptions(upsert=True)
147
+ )
148
+ self._index_loaded = False
149
+ await self._load_index()
150
+
151
+ # ------------------------------------------------------------------
152
+ # Lifecycle
153
+ # ------------------------------------------------------------------
154
+
155
+ def create(self) -> None:
156
+ """Load the index into memory if it already exists."""
157
+ if self.exists():
158
+ self._run(self._load_index())
159
+
160
+ async def async_create(self) -> None:
161
+ """Async variant of create()."""
162
+ if await self.async_exists():
163
+ await self._load_index()
164
+
165
+ def drop(self) -> None:
166
+ """Delete the Moss index and all its data."""
167
+ try:
168
+ self._run(self._client.delete_index(self.index_name))
169
+ self._index_loaded = False
170
+ log_info(f"Deleted Moss index '{self.index_name}'")
171
+ except Exception as e:
172
+ log_error(f"Error deleting Moss index '{self.index_name}': {e}")
173
+
174
+ async def async_drop(self) -> None:
175
+ """Async variant of drop()."""
176
+ try:
177
+ await self._client.delete_index(self.index_name)
178
+ self._index_loaded = False
179
+ log_info(f"Deleted Moss index '{self.index_name}'")
180
+ except Exception as e:
181
+ log_error(f"Error deleting Moss index '{self.index_name}': {e}")
182
+
183
+ # ------------------------------------------------------------------
184
+ # Existence checks
185
+ # ------------------------------------------------------------------
186
+
187
+ def exists(self) -> bool:
188
+ """Return True if the index exists in the project."""
189
+ try:
190
+ indexes = self._run(self._client.list_indexes())
191
+ return any(idx.name == self.index_name for idx in indexes)
192
+ except Exception:
193
+ return False
194
+
195
+ async def async_exists(self) -> bool:
196
+ """Async variant of exists()."""
197
+ try:
198
+ indexes = await self._client.list_indexes()
199
+ return any(idx.name == self.index_name for idx in indexes)
200
+ except Exception:
201
+ return False
202
+
203
+ def name_exists(self, name: str) -> bool:
204
+ """Return True if this VectorDb manages the given index name."""
205
+ return name == self.index_name and self.exists()
206
+
207
+ async def async_name_exists(self, name: str) -> bool:
208
+ """Async variant of name_exists()."""
209
+ return name == self.index_name and await self.async_exists()
210
+
211
+ def id_exists(self, id: str) -> bool:
212
+ """Return True if a document with the given ID exists in the index."""
213
+ try:
214
+ docs = self._run(
215
+ self._client.get_docs(self.index_name, GetDocumentsOptions(doc_ids=[id]))
216
+ )
217
+ return bool(docs)
218
+ except Exception:
219
+ return False
220
+
221
+ def content_hash_exists(self, content_hash: str) -> bool:
222
+ """Return True if a document with the given content hash exists.
223
+
224
+ Metadata filtering requires a locally loaded index. Returns False
225
+ (safe: forces re-upsert) when the index is not yet loaded.
226
+ """
227
+ if not self._index_loaded:
228
+ return False
229
+ try:
230
+ results = self._run(
231
+ self._client.query(
232
+ self.index_name,
233
+ content_hash,
234
+ options=QueryOptions(
235
+ alpha=0.0,
236
+ top_k=1,
237
+ filter={"field": "content_hash", "condition": {"$eq": content_hash}},
238
+ ),
239
+ )
240
+ )
241
+ return bool(results and results.docs)
242
+ except Exception:
243
+ return False
244
+
245
+ # ------------------------------------------------------------------
246
+ # Insert / Upsert
247
+ # ------------------------------------------------------------------
248
+
249
+ def upsert_available(self) -> bool:
250
+ """Moss supports upsert natively."""
251
+ return True
252
+
253
+ def insert(
254
+ self,
255
+ content_hash: str,
256
+ documents: list[Document],
257
+ filters: Any | None = None,
258
+ ) -> None:
259
+ """Insert documents; delegates to upsert."""
260
+ self.upsert(content_hash=content_hash, documents=documents, filters=filters)
261
+
262
+ async def async_insert(
263
+ self,
264
+ content_hash: str,
265
+ documents: list[Document],
266
+ filters: Any | None = None,
267
+ ) -> None:
268
+ """Async variant of insert()."""
269
+ await self.async_upsert(content_hash=content_hash, documents=documents, filters=filters)
270
+
271
+ def upsert(
272
+ self,
273
+ content_hash: str,
274
+ documents: list[Document],
275
+ filters: Any | None = None,
276
+ ) -> None:
277
+ """Upsert documents into the index, creating it if necessary."""
278
+ moss_docs = [self._to_moss_doc(doc, content_hash) for doc in documents]
279
+ if not moss_docs:
280
+ return
281
+ try:
282
+ self._run(self._upsert_docs(moss_docs))
283
+ log_info(f"Upserted {len(moss_docs)} documents into Moss index '{self.index_name}'")
284
+ except Exception as e:
285
+ log_error(f"Error upserting documents into Moss: {e}")
286
+
287
+ async def async_upsert(
288
+ self,
289
+ content_hash: str,
290
+ documents: list[Document],
291
+ filters: Any | None = None,
292
+ ) -> None:
293
+ """Async variant of upsert()."""
294
+ moss_docs = [self._to_moss_doc(doc, content_hash) for doc in documents]
295
+ if not moss_docs:
296
+ return
297
+ try:
298
+ await self._upsert_docs(moss_docs)
299
+ log_info(f"Upserted {len(moss_docs)} documents into Moss index '{self.index_name}'")
300
+ except Exception as e:
301
+ log_error(f"Error upserting documents into Moss: {e}")
302
+
303
+ # ------------------------------------------------------------------
304
+ # Search
305
+ # ------------------------------------------------------------------
306
+
307
+ def search(self, query: str, limit: int = 5, filters: Any | None = None) -> list[Document]:
308
+ """Search the index and return the top matching documents."""
309
+ try:
310
+ results = self._run(
311
+ self._client.query(
312
+ self.index_name,
313
+ query,
314
+ options=QueryOptions(top_k=limit, alpha=self.alpha, filter=filters),
315
+ )
316
+ )
317
+ if not results or not results.docs:
318
+ return []
319
+ docs = [self._to_document(r) for r in results.docs]
320
+ log_debug(f"Moss search returned {len(docs)} results for '{query}'")
321
+ return docs
322
+ except Exception as e:
323
+ log_error(f"Error searching Moss index '{self.index_name}': {e}")
324
+ return []
325
+
326
+ async def async_search(
327
+ self, query: str, limit: int = 5, filters: Any | None = None
328
+ ) -> list[Document]:
329
+ """Async variant of search()."""
330
+ try:
331
+ results = await self._client.query(
332
+ self.index_name,
333
+ query,
334
+ options=QueryOptions(top_k=limit, alpha=self.alpha, filter=filters),
335
+ )
336
+ if not results or not results.docs:
337
+ return []
338
+ docs = [self._to_document(r) for r in results.docs]
339
+ log_debug(f"Moss async_search returned {len(docs)} results for '{query}'")
340
+ return docs
341
+ except Exception as e:
342
+ log_error(f"Error searching Moss index '{self.index_name}': {e}")
343
+ return []
344
+
345
+ # ------------------------------------------------------------------
346
+ # Deletion
347
+ # ------------------------------------------------------------------
348
+
349
+ def delete(self) -> bool:
350
+ """Delete the entire index. Returns True on success."""
351
+ try:
352
+ self._run(self._client.delete_index(self.index_name))
353
+ self._index_loaded = False
354
+ return True
355
+ except Exception as e:
356
+ log_error(f"Error deleting Moss index: {e}")
357
+ return False
358
+
359
+ def delete_by_id(self, id: str) -> bool:
360
+ """Delete a single document by ID."""
361
+ try:
362
+ self._run(self._client.delete_docs(self.index_name, [id]))
363
+ return True
364
+ except Exception as e:
365
+ log_error(f"Error deleting document '{id}' from Moss: {e}")
366
+ return False
367
+
368
+ def delete_by_name(self, name: str) -> bool:
369
+ """Not supported — Moss does not index by document name."""
370
+ log_warning("delete_by_name is not supported by MossRuntime.")
371
+ return False
372
+
373
+ def delete_by_metadata(self, metadata: dict[str, Any]) -> bool:
374
+ """Not supported — use delete_by_id() for targeted removal."""
375
+ log_warning("delete_by_metadata is not supported by MossRuntime.")
376
+ return False
377
+
378
+ def delete_by_content_id(self, content_id: str) -> bool:
379
+ """Not supported — use delete_by_id() for targeted removal."""
380
+ log_warning("delete_by_content_id is not supported by MossRuntime.")
381
+ return False
382
+
383
+ # ------------------------------------------------------------------
384
+ # Misc
385
+ # ------------------------------------------------------------------
386
+
387
+ def optimize(self) -> None:
388
+ """No-op: Moss manages its own index optimization."""
389
+
390
+ def get_supported_search_types(self) -> list[str]:
391
+ """Return the search types supported by Moss."""
392
+ return ["vector", "keyword", "hybrid"]
@@ -0,0 +1,105 @@
1
+ Metadata-Version: 2.4
2
+ Name: agno-moss
3
+ Version: 0.0.1
4
+ Summary: Moss semantic search integration for Agno agents
5
+ License: BSD-2-Clause
6
+ Requires-Python: <3.15,>=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: moss>=1.1.1
10
+ Requires-Dist: agno>=2.5.17
11
+ Dynamic: license-file
12
+
13
+ # agno-moss
14
+
15
+ The Moss in-memory semantic search runtime for [Agno](https://docs.agno.com) agents.
16
+
17
+ Moss manages embeddings internally and serves queries from an in-memory runtime — sub-10ms lookups, no external embedder, no vector database to run. Point `Knowledge` at `MossRuntime` and Agno agents get instant RAG with zero infrastructure.
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pip install agno-moss
23
+ # or
24
+ uv add agno-moss
25
+ ```
26
+
27
+ ## Prerequisites
28
+
29
+ - Moss project ID and project key — get them from the [Moss Portal](https://portal.usemoss.dev)
30
+ - Python 3.10+
31
+ - An Agno-compatible model provider (OpenAI, Anthropic, etc.)
32
+
33
+ ## Quickstart
34
+
35
+ ```python
36
+ import os
37
+ from agno.agent import Agent
38
+ from agno.knowledge.knowledge import Knowledge
39
+ from agno.models.anthropic import Claude
40
+ from agno_moss import MossRuntime
41
+
42
+ knowledge = Knowledge(
43
+ vector_db=MossRuntime(
44
+ index_name="my-index",
45
+ # Falls back to MOSS_PROJECT_ID / MOSS_PROJECT_KEY env vars
46
+ ),
47
+ )
48
+
49
+ agent = Agent(
50
+ model=Claude(id="claude-sonnet-4-20250514"),
51
+ knowledge=knowledge,
52
+ search_knowledge=True,
53
+ markdown=True,
54
+ )
55
+
56
+ knowledge.load(recreate=False)
57
+ agent.print_response("What do you know about our return policy?", stream=True)
58
+ ```
59
+
60
+ ## Configuration
61
+
62
+ ### MossRuntime
63
+
64
+ | Parameter | Default | Description |
65
+ |---|---|---|
66
+ | `index_name` | (required) | Name of the Moss index |
67
+ | `project_id` | `MOSS_PROJECT_ID` env var | Moss project ID |
68
+ | `project_key` | `MOSS_PROJECT_KEY` env var | Moss project key |
69
+ | `embedding_model` | `"moss-minilm"` | `"moss-minilm"` (fast) or `"moss-mediumlm"` (higher accuracy) |
70
+ | `alpha` | `0.8` | Hybrid search blend: 1.0 = semantic only, 0.0 = keyword only |
71
+ | `auto_refresh` | `False` | Auto-refresh the in-memory index when new docs are added |
72
+ | `polling_interval_in_seconds` | `600` | Refresh interval when `auto_refresh=True` |
73
+
74
+ ## How it works
75
+
76
+ `MossRuntime` implements Agno's `VectorDb` base class:
77
+
78
+ - **`create()`** — loads an existing index into Moss's in-memory runtime. Call once at startup for fast first queries.
79
+ - **`upsert()`** — creates the index on first call, then adds or updates documents. Loads the index automatically after each batch.
80
+ - **`search()`** — hybrid semantic + keyword search via the loaded in-memory runtime. Falls back to the cloud API if the index is not loaded.
81
+
82
+ Moss filters metadata **only when the index is loaded locally**. `content_hash_exists()` returns `False` when unloaded (safe: forces re-upsert rather than silently skipping).
83
+
84
+ ## Choosing a model provider
85
+
86
+ ```python
87
+ # OpenAI
88
+ from agno.models.openai import OpenAIChat
89
+ agent = Agent(model=OpenAIChat(id="gpt-4o"), knowledge=knowledge, search_knowledge=True)
90
+
91
+ # Anthropic
92
+ from agno.models.anthropic import Claude
93
+ agent = Agent(model=Claude(id="claude-sonnet-4-20250514"), knowledge=knowledge, search_knowledge=True)
94
+ ```
95
+
96
+ See the [Agno model providers docs](https://docs.agno.com/models/introduction) for the full list.
97
+
98
+ ## License
99
+
100
+ BSD 2-Clause — see [LICENSE](LICENSE).
101
+
102
+ ## Support
103
+
104
+ - [Moss Docs](https://docs.moss.dev)
105
+ - [Agno Docs](https://docs.agno.com)
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/agno_moss/__init__.py
5
+ src/agno_moss/runtime.py
6
+ src/agno_moss.egg-info/PKG-INFO
7
+ src/agno_moss.egg-info/SOURCES.txt
8
+ src/agno_moss.egg-info/dependency_links.txt
9
+ src/agno_moss.egg-info/requires.txt
10
+ src/agno_moss.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ moss>=1.1.1
2
+ agno>=2.5.17
@@ -0,0 +1 @@
1
+ agno_moss