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.
- agno_moss-0.0.1/LICENSE +25 -0
- agno_moss-0.0.1/PKG-INFO +105 -0
- agno_moss-0.0.1/README.md +93 -0
- agno_moss-0.0.1/pyproject.toml +51 -0
- agno_moss-0.0.1/setup.cfg +4 -0
- agno_moss-0.0.1/src/agno_moss/__init__.py +22 -0
- agno_moss-0.0.1/src/agno_moss/runtime.py +392 -0
- agno_moss-0.0.1/src/agno_moss.egg-info/PKG-INFO +105 -0
- agno_moss-0.0.1/src/agno_moss.egg-info/SOURCES.txt +10 -0
- agno_moss-0.0.1/src/agno_moss.egg-info/dependency_links.txt +1 -0
- agno_moss-0.0.1/src/agno_moss.egg-info/requires.txt +2 -0
- agno_moss-0.0.1/src/agno_moss.egg-info/top_level.txt +1 -0
agno_moss-0.0.1/LICENSE
ADDED
|
@@ -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.
|
agno_moss-0.0.1/PKG-INFO
ADDED
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
agno_moss
|