moss-agent 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,168 @@
1
+ Copyright (c) 2025 InferEdge Inc.
2
+ Required Notice: Copyright InferEdge Inc. (https://inferedge.dev)
3
+ Licensor Line of Business: InferEdge MOSS Runtime (https://inferedge.dev)
4
+
5
+ # PolyForm Shield License 1.0.0
6
+
7
+ <https://polyformproject.org/licenses/shield/1.0.0>
8
+
9
+ ## Acceptance
10
+
11
+ In order to get any license under these terms, you must agree
12
+ to them as both strict obligations and conditions to all
13
+ your licenses.
14
+
15
+ ## Copyright License
16
+
17
+ The licensor grants you a copyright license for the
18
+ software to do everything you might do with the software
19
+ that would otherwise infringe the licensor's copyright
20
+ in it for any permitted purpose. However, you may
21
+ only distribute the software according to [Distribution
22
+ License](#distribution-license) and make changes or new works
23
+ based on the software according to [Changes and New Works
24
+ License](#changes-and-new-works-license).
25
+
26
+ ## Distribution License
27
+
28
+ The licensor grants you an additional copyright license
29
+ to distribute copies of the software. Your license
30
+ to distribute covers distributing the software with
31
+ changes and new works permitted by [Changes and New Works
32
+ License](#changes-and-new-works-license).
33
+
34
+ ## Notices
35
+
36
+ You must ensure that anyone who gets a copy of any part of
37
+ the software from you also gets a copy of these terms or the
38
+ URL for them above, as well as copies of any plain-text lines
39
+ beginning with `Required Notice:` that the licensor provided
40
+ with the software. For example:
41
+
42
+ > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
43
+
44
+ ## Changes and New Works License
45
+
46
+ The licensor grants you an additional copyright license to
47
+ make changes and new works based on the software for any
48
+ permitted purpose.
49
+
50
+ ## Patent License
51
+
52
+ The licensor grants you a patent license for the software that
53
+ covers patent claims the licensor can license, or becomes able
54
+ to license, that you would infringe by using the software.
55
+
56
+ ## Noncompete
57
+
58
+ Any purpose is a permitted purpose, except for providing any
59
+ product that competes with the software or any product the
60
+ licensor or any of its affiliates provides using the software.
61
+
62
+ ## Competition
63
+
64
+ Goods and services compete even when they provide functionality
65
+ through different kinds of interfaces or for different technical
66
+ platforms. Applications can compete with services, libraries
67
+ with plugins, frameworks with development tools, and so on,
68
+ even if they're written in different programming languages
69
+ or for different computer architectures. Goods and services
70
+ compete even when provided free of charge. If you market a
71
+ product as a practical substitute for the software or another
72
+ product, it definitely competes.
73
+
74
+ ## New Products
75
+
76
+ If you are using the software to provide a product that does
77
+ not compete, but the licensor or any of its affiliates brings
78
+ your product into competition by providing a new version of
79
+ the software or another product using the software, you may
80
+ continue using versions of the software available under these
81
+ terms beforehand to provide your competing product, but not
82
+ any later versions.
83
+
84
+ ## Discontinued Products
85
+
86
+ You may begin using the software to compete with a product
87
+ or service that the licensor or any of its affiliates has
88
+ stopped providing, unless the licensor includes a plain-text
89
+ line beginning with `Licensor Line of Business:` with the
90
+ software that mentions that line of business. For example:
91
+
92
+ > Licensor Line of Business: YoyodyneCMS Content Management
93
+ System (http://example.com/cms)
94
+
95
+ ## Sales of Business
96
+
97
+ If the licensor or any of its affiliates sells a line of
98
+ business developing the software or using the software
99
+ to provide a product, the buyer can also enforce
100
+ [Noncompete](#noncompete) for that product.
101
+
102
+ ## Fair Use
103
+
104
+ You may have "fair use" rights for the software under the
105
+ law. These terms do not limit them.
106
+
107
+ ## No Other Rights
108
+
109
+ These terms do not allow you to sublicense or transfer any of
110
+ your licenses to anyone else, or prevent the licensor from
111
+ granting licenses to anyone else. These terms do not imply
112
+ any other licenses.
113
+
114
+ ## Patent Defense
115
+
116
+ If you make any written claim that the software infringes or
117
+ contributes to infringement of any patent, your patent license
118
+ for the software granted under these terms ends immediately. If
119
+ your company makes such a claim, your patent license ends
120
+ immediately for work on behalf of your company.
121
+
122
+ ## Violations
123
+
124
+ The first time you are notified in writing that you have
125
+ violated any of these terms, or done anything with the software
126
+ not covered by your licenses, your licenses can nonetheless
127
+ continue if you come into full compliance with these terms,
128
+ and take practical steps to correct past violations, within
129
+ 32 days of receiving notice. Otherwise, all your licenses
130
+ end immediately.
131
+
132
+ ## No Liability
133
+
134
+ ***As far as the law allows, the software comes as is, without
135
+ any warranty or condition, and the licensor will not be liable
136
+ to you for any damages arising out of these terms or the use
137
+ or nature of the software, under any kind of legal claim.***
138
+
139
+ ## Definitions
140
+
141
+ The **licensor** is the individual or entity offering these
142
+ terms, and the **software** is the software the licensor makes
143
+ available under these terms.
144
+
145
+ A **product** can be a good or service, or a combination
146
+ of them.
147
+
148
+ **You** refers to the individual or entity agreeing to these
149
+ terms.
150
+
151
+ **Your company** is any legal entity, sole proprietorship,
152
+ or other kind of organization that you work for, plus all
153
+ its affiliates.
154
+
155
+ **Affiliates** means the other organizations than an
156
+ organization has control over, is under the control of, or is
157
+ under common control with.
158
+
159
+ **Control** means ownership of substantially all the assets of
160
+ an entity, or the power to direct its management and policies
161
+ by vote, contract, or otherwise. Control can be direct or
162
+ indirect.
163
+
164
+ **Your licenses** are all the licenses granted to you for the
165
+ software under these terms.
166
+
167
+ **Use** means anything you do with the software requiring one
168
+ of your licenses.
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: moss-agent
3
+ Version: 1.0.0
4
+ Summary: Moss runtime for LiveKit voice agents - a hot index cache shared across rooms and a one-line attach per call.
5
+ Author-email: "InferEdge Inc." <contact@usemoss.dev>
6
+ Project-URL: Homepage, https://github.com/usemoss/moss-samples
7
+ Project-URL: Repository, https://github.com/usemoss/moss-samples
8
+ Project-URL: Documentation, https://docs.usemoss.dev/
9
+ Keywords: livekit,voice-agent,semantic-search,moss,usemoss
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE.txt
22
+ Requires-Dist: typing-extensions>=4.0.0
23
+ Requires-Dist: inferedge-moss-core==0.12.0
24
+ Provides-Extra: livekit
25
+ Requires-Dist: livekit-agents>=0.10.0; extra == "livekit"
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8.4.2; extra == "dev"
28
+ Requires-Dist: pytest-asyncio>=1.2.0; extra == "dev"
29
+ Requires-Dist: black>=25.9.0; extra == "dev"
30
+ Requires-Dist: isort>=7.0.0; extra == "dev"
31
+ Requires-Dist: mypy>=1.18.2; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # moss-agent
35
+
36
+ Moss runtime for LiveKit voice agents - a hot index cache shared across rooms and a one-line attach per call.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install moss-agent
42
+ ```
43
+
44
+ ## Use - voice (LiveKit)
45
+
46
+ ```python
47
+ from livekit.agents import AgentServer, JobContext, JobProcess
48
+ from moss_agent import MossAgent
49
+
50
+ server = AgentServer()
51
+
52
+
53
+ def prewarm(proc: JobProcess) -> None:
54
+ agent = MossAgent(
55
+ project_id="...",
56
+ project_key="...",
57
+ )
58
+ import asyncio
59
+ asyncio.run(agent.load_indexes(["product_catalog", "faq", "policies"]))
60
+ proc.userdata["moss_agent"] = agent
61
+
62
+
63
+ server.setup_fnc = prewarm
64
+
65
+
66
+ @server.rtc_session(agent_name="my-agent")
67
+ async def handle_visit(ctx: JobContext) -> None:
68
+ await ctx.connect()
69
+
70
+ agent: MossAgent = ctx.proc.userdata["moss_agent"]
71
+ call = agent.attach(ctx)
72
+
73
+ results = await call.query("product_catalog", user_question)
74
+ # ... your existing agent loop ...
75
+ ```
76
+
77
+ ## Use - text (HTTP / chat / non-LiveKit)
78
+
79
+ ```python
80
+ from moss_agent import MossAgent
81
+
82
+ # Module level - reuse across requests
83
+ agent = MossAgent(project_id="...", project_key="...")
84
+ await agent.load_indexes(["product_catalog"])
85
+
86
+
87
+ # In your HTTP / chat handler
88
+ async def handle_message(req):
89
+ results = await agent.query("product_catalog", req.user_message)
90
+ # ... feed results to your model ...
91
+ ```
92
+
93
+ ## API surface
94
+
95
+ - `MossAgent(project_id, project_key)` - process-wide instance. Build once, share across every room (voice) or every request (text).
96
+ - **Voice:** `MossAgent.attach(ctx) -> MossCall` binds a Moss call scope to a LiveKit `JobContext`. Idempotent on `ctx.room.name`. `MossCall.query(name, query, options=None)` and `MossCall.query_multi_index(names, query, options=None)` are the call-scoped query surface.
97
+ - **Text:** `MossAgent.query(name, query, options=None)` and `MossAgent.query_multi_index(names, query, options=None)`.
98
+ - Full index CRUD (`create_index`, `add_docs`, `delete_docs`, `delete_index`, `list_indexes`, `get_index`, `get_docs`, `get_job_status`) and cache lifecycle (`load_index`, `load_indexes`, `unload_index`, `unload_indexes`) on `MossAgent`.
99
+
100
+ ## Requirements
101
+
102
+ - Python 3.10+
103
+ - `inferedge-moss-core == 0.12.0`
104
+ - `livekit-agents >= 0.10.0` (only required if you call `attach()`; install via the `[livekit]` extra)
105
+
106
+ ## License
107
+
108
+ Proprietary. See `LICENSE.txt`.
@@ -0,0 +1,75 @@
1
+ # moss-agent
2
+
3
+ Moss runtime for LiveKit voice agents - a hot index cache shared across rooms and a one-line attach per call.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install moss-agent
9
+ ```
10
+
11
+ ## Use - voice (LiveKit)
12
+
13
+ ```python
14
+ from livekit.agents import AgentServer, JobContext, JobProcess
15
+ from moss_agent import MossAgent
16
+
17
+ server = AgentServer()
18
+
19
+
20
+ def prewarm(proc: JobProcess) -> None:
21
+ agent = MossAgent(
22
+ project_id="...",
23
+ project_key="...",
24
+ )
25
+ import asyncio
26
+ asyncio.run(agent.load_indexes(["product_catalog", "faq", "policies"]))
27
+ proc.userdata["moss_agent"] = agent
28
+
29
+
30
+ server.setup_fnc = prewarm
31
+
32
+
33
+ @server.rtc_session(agent_name="my-agent")
34
+ async def handle_visit(ctx: JobContext) -> None:
35
+ await ctx.connect()
36
+
37
+ agent: MossAgent = ctx.proc.userdata["moss_agent"]
38
+ call = agent.attach(ctx)
39
+
40
+ results = await call.query("product_catalog", user_question)
41
+ # ... your existing agent loop ...
42
+ ```
43
+
44
+ ## Use - text (HTTP / chat / non-LiveKit)
45
+
46
+ ```python
47
+ from moss_agent import MossAgent
48
+
49
+ # Module level - reuse across requests
50
+ agent = MossAgent(project_id="...", project_key="...")
51
+ await agent.load_indexes(["product_catalog"])
52
+
53
+
54
+ # In your HTTP / chat handler
55
+ async def handle_message(req):
56
+ results = await agent.query("product_catalog", req.user_message)
57
+ # ... feed results to your model ...
58
+ ```
59
+
60
+ ## API surface
61
+
62
+ - `MossAgent(project_id, project_key)` - process-wide instance. Build once, share across every room (voice) or every request (text).
63
+ - **Voice:** `MossAgent.attach(ctx) -> MossCall` binds a Moss call scope to a LiveKit `JobContext`. Idempotent on `ctx.room.name`. `MossCall.query(name, query, options=None)` and `MossCall.query_multi_index(names, query, options=None)` are the call-scoped query surface.
64
+ - **Text:** `MossAgent.query(name, query, options=None)` and `MossAgent.query_multi_index(names, query, options=None)`.
65
+ - Full index CRUD (`create_index`, `add_docs`, `delete_docs`, `delete_index`, `list_indexes`, `get_index`, `get_docs`, `get_job_status`) and cache lifecycle (`load_index`, `load_indexes`, `unload_index`, `unload_indexes`) on `MossAgent`.
66
+
67
+ ## Requirements
68
+
69
+ - Python 3.10+
70
+ - `inferedge-moss-core == 0.12.0`
71
+ - `livekit-agents >= 0.10.0` (only required if you call `attach()`; install via the `[livekit]` extra)
72
+
73
+ ## License
74
+
75
+ Proprietary. See `LICENSE.txt`.
@@ -0,0 +1,73 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "moss-agent"
7
+ version = "1.0.0"
8
+ description = "Moss runtime for LiveKit voice agents - a hot index cache shared across rooms and a one-line attach per call."
9
+ readme = "README.md"
10
+ license-files = ["LICENSE.txt"]
11
+ authors = [
12
+ { name = "InferEdge Inc.", email = "contact@usemoss.dev" }
13
+ ]
14
+ keywords = ["livekit", "voice-agent", "semantic-search", "moss", "usemoss"]
15
+ classifiers = [
16
+ "Development Status :: 5 - Production/Stable",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Topic :: Software Development :: Libraries :: Python Modules",
24
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
25
+ ]
26
+ requires-python = ">=3.10"
27
+ dependencies = [
28
+ "typing-extensions>=4.0.0",
29
+ "inferedge-moss-core==0.12.0",
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ livekit = [
34
+ "livekit-agents>=0.10.0",
35
+ ]
36
+ dev = [
37
+ "pytest>=8.4.2",
38
+ "pytest-asyncio>=1.2.0",
39
+ "black>=25.9.0",
40
+ "isort>=7.0.0",
41
+ "mypy>=1.18.2",
42
+ ]
43
+
44
+ [project.urls]
45
+ Homepage = "https://github.com/usemoss/moss-samples"
46
+ Repository = "https://github.com/usemoss/moss-samples"
47
+ Documentation = "https://docs.usemoss.dev/"
48
+
49
+ [tool.setuptools.packages.find]
50
+ where = ["src"]
51
+
52
+ [tool.setuptools.package-dir]
53
+ "" = "src"
54
+
55
+ [tool.setuptools.package-data]
56
+ "moss_agent" = [
57
+ "py.typed",
58
+ ]
59
+
60
+ [tool.black]
61
+ line-length = 88
62
+ target-version = ['py310']
63
+
64
+ [tool.isort]
65
+ profile = "black"
66
+ line_length = 88
67
+
68
+ [tool.mypy]
69
+ python_version = "3.10"
70
+ warn_return_any = false
71
+ warn_unused_configs = true
72
+ disallow_untyped_defs = true
73
+ ignore_missing_imports = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,82 @@
1
+ """
2
+ Moss Agent SDK - Moss for LiveKit voice agents.
3
+
4
+ A single :class:`MossAgent` lives at the process level, holding a hot
5
+ index cache that every room the worker handles queries against - no
6
+ per-call cold start, no duplicated state, one warm in-memory index
7
+ serving hundreds of concurrent rooms.
8
+
9
+ Two query surfaces share the same agent:
10
+
11
+ * **Voice / LiveKit**: :meth:`MossAgent.attach` binds a
12
+ :class:`MossCall` to the room; queries via :meth:`MossCall.query`.
13
+ * **Text / HTTP / chat**: :meth:`MossAgent.query` directly.
14
+
15
+ Example - voice agent::
16
+
17
+ from moss_agent import MossAgent
18
+
19
+ # In prewarm():
20
+ agent = MossAgent("project-id", "project-key")
21
+ await agent.load_indexes(["kb1", "kb2"])
22
+ proc.userdata["moss_agent"] = agent
23
+
24
+ # In handle_visit(ctx):
25
+ await ctx.connect()
26
+ call = agent.attach(ctx)
27
+ results = await call.query("kb1", question)
28
+
29
+ Example - text agent::
30
+
31
+ agent = MossAgent("project-id", "project-key")
32
+ await agent.load_indexes(["kb1"])
33
+
34
+ # In an HTTP handler or chat message handler:
35
+ results = await agent.query("kb1", user_message)
36
+ """
37
+
38
+ from moss_core import (
39
+ DocumentInfo,
40
+ GetDocumentsOptions,
41
+ IndexInfo,
42
+ IndexStatus,
43
+ IndexStatusValues,
44
+ JobPhase,
45
+ JobProgress,
46
+ JobStatus,
47
+ JobStatusResponse,
48
+ LoadIndexesResult,
49
+ ModelRef,
50
+ MutationOptions,
51
+ MutationResult,
52
+ QueryOptions,
53
+ QueryResultDocumentInfo,
54
+ SearchResult,
55
+ )
56
+
57
+ from .client import MossAgent, MossCall, ParseFileInput
58
+
59
+ __version__ = "1.0.0"
60
+
61
+ __all__ = [
62
+ "MossAgent",
63
+ "MossCall",
64
+ "ParseFileInput",
65
+ # Re-exports from moss_core
66
+ "DocumentInfo",
67
+ "GetDocumentsOptions",
68
+ "IndexInfo",
69
+ "IndexStatus",
70
+ "IndexStatusValues",
71
+ "JobPhase",
72
+ "JobProgress",
73
+ "JobStatus",
74
+ "JobStatusResponse",
75
+ "LoadIndexesResult",
76
+ "ModelRef",
77
+ "MutationOptions",
78
+ "MutationResult",
79
+ "QueryOptions",
80
+ "QueryResultDocumentInfo",
81
+ "SearchResult",
82
+ ]
@@ -0,0 +1,404 @@
1
+ """
2
+ Python wrappers over the Rust ``moss_core`` Agent + CallScope.
3
+
4
+ This module is intentionally thin: each method either delegates to the
5
+ underlying Rust binding via :func:`asyncio.to_thread` (so the sync Rust
6
+ call doesn't block the asyncio event loop) or unpacks framework-specific
7
+ inputs (a LiveKit ``JobContext``) into the opaque correlation strings the
8
+ Rust layer expects.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ from dataclasses import dataclass
15
+ from typing import Any, List, Optional
16
+
17
+ from moss_core import (
18
+ Agent as _Agent,
19
+ DocumentInfo,
20
+ GetDocumentsOptions,
21
+ IndexInfo,
22
+ JobStatusResponse,
23
+ LoadIndexesResult,
24
+ MutationOptions,
25
+ MutationResult,
26
+ QueryOptions,
27
+ SearchResult,
28
+ )
29
+
30
+
31
+ @dataclass
32
+ class ParseFileInput:
33
+ """
34
+ Input descriptor for a single file in the parse pipeline.
35
+
36
+ Either ``path`` (filesystem path) or ``data`` (raw bytes) must be
37
+ provided. Both ``name`` and ``content_type`` are required. Only
38
+ ``"application/pdf"`` is currently supported as ``content_type``.
39
+ """
40
+
41
+ name: str
42
+ content_type: str
43
+ path: Optional[str] = None
44
+ data: Optional[bytes] = None
45
+
46
+
47
+ class MossAgent:
48
+ """
49
+ Process-wide Moss runtime for an agent process.
50
+
51
+ Owns the local index cache and auth. One per process - typically
52
+ constructed in LiveKit's ``prewarm()`` and stashed on
53
+ ``proc.userdata`` for every room handler to share.
54
+
55
+ Two query surfaces, one object:
56
+
57
+ * Voice / LiveKit: call :meth:`attach` with a ``JobContext`` to get a
58
+ :class:`MossCall`, then use :meth:`MossCall.query`.
59
+
60
+ * Text / HTTP / chat: call :meth:`query` (or :meth:`query_multi_index`)
61
+ directly on the agent.
62
+ """
63
+
64
+ DEFAULT_MODEL_ID = "moss-minilm"
65
+
66
+ def __init__(
67
+ self,
68
+ project_id: str,
69
+ project_key: str,
70
+ base_url: Optional[str] = None,
71
+ client_id: Optional[str] = None,
72
+ ) -> None:
73
+ self._inner = _Agent(
74
+ project_id,
75
+ project_key,
76
+ base_url=base_url,
77
+ client_id=client_id,
78
+ )
79
+
80
+ @property
81
+ def client_id(self) -> str:
82
+ return self._inner.client_id()
83
+
84
+ async def active_call_count(self) -> int:
85
+ """Number of currently-active call scopes (across all rooms)."""
86
+ return await asyncio.to_thread(self._inner.active_call_count)
87
+
88
+ # -- Index CRUD ---------------------------------------------------
89
+
90
+ async def create_index(
91
+ self,
92
+ name: str,
93
+ docs: List[DocumentInfo],
94
+ model_id: Optional[str] = None,
95
+ ) -> MutationResult:
96
+ resolved = self._resolve_model_id(docs, model_id)
97
+ return await asyncio.to_thread(self._inner.create_index, name, docs, resolved)
98
+
99
+ async def create_index_from_files(
100
+ self,
101
+ name: str,
102
+ files: List[ParseFileInput],
103
+ model_id: Optional[str] = None,
104
+ ) -> MutationResult:
105
+ resolved = model_id or self.DEFAULT_MODEL_ID
106
+ if resolved == "custom":
107
+ raise ValueError(
108
+ "create_index_from_files does not support model_id='custom' - "
109
+ "the parse pipeline generates embeddings server-side."
110
+ )
111
+ from moss_core import ParseFileInput as CoreParseFileInput
112
+
113
+ core_files = [
114
+ CoreParseFileInput(f.name, f.content_type, path=f.path, data=f.data)
115
+ for f in files
116
+ ]
117
+ return await asyncio.to_thread(
118
+ self._inner.create_index_from_files, name, core_files, resolved
119
+ )
120
+
121
+ async def add_docs(
122
+ self,
123
+ name: str,
124
+ docs: List[DocumentInfo],
125
+ options: Optional[MutationOptions] = None,
126
+ ) -> MutationResult:
127
+ return await asyncio.to_thread(self._inner.add_docs, name, docs, options)
128
+
129
+ async def delete_docs(self, name: str, doc_ids: List[str]) -> MutationResult:
130
+ return await asyncio.to_thread(self._inner.delete_docs, name, doc_ids)
131
+
132
+ async def delete_index(self, name: str) -> bool:
133
+ return await asyncio.to_thread(self._inner.delete_index, name)
134
+
135
+ async def list_indexes(self) -> List[IndexInfo]:
136
+ return await asyncio.to_thread(self._inner.list_indexes)
137
+
138
+ async def get_index(self, name: str) -> IndexInfo:
139
+ return await asyncio.to_thread(self._inner.get_index, name)
140
+
141
+ async def get_docs(
142
+ self,
143
+ name: str,
144
+ options: Optional[GetDocumentsOptions] = None,
145
+ ) -> List[DocumentInfo]:
146
+ return await asyncio.to_thread(self._inner.get_docs, name, options)
147
+
148
+ async def get_job_status(self, job_id: str) -> JobStatusResponse:
149
+ return await asyncio.to_thread(self._inner.get_job_status, job_id)
150
+
151
+ # -- Index lifecycle ----------------------------------------------
152
+
153
+ async def load_index(
154
+ self,
155
+ name: str,
156
+ auto_refresh: bool = False,
157
+ polling_interval_in_seconds: int = 600,
158
+ cache_path: Optional[str] = None,
159
+ ) -> IndexInfo:
160
+ return await asyncio.to_thread(
161
+ self._inner.load_index,
162
+ name,
163
+ auto_refresh,
164
+ polling_interval_in_seconds,
165
+ cache_path,
166
+ )
167
+
168
+ async def load_indexes(
169
+ self,
170
+ names: List[str],
171
+ auto_refresh: bool = False,
172
+ polling_interval_in_seconds: int = 600,
173
+ cache_path: Optional[str] = None,
174
+ ) -> LoadIndexesResult:
175
+ return await asyncio.to_thread(
176
+ self._inner.load_indexes,
177
+ names,
178
+ auto_refresh,
179
+ polling_interval_in_seconds,
180
+ cache_path,
181
+ )
182
+
183
+ async def unload_index(self, name: str) -> None:
184
+ await asyncio.to_thread(self._inner.unload_index, name)
185
+
186
+ async def unload_indexes(self, names: List[str]) -> None:
187
+ await asyncio.to_thread(self._inner.unload_indexes, names)
188
+
189
+ # -- Call lifecycle -----------------------------------------------
190
+
191
+ def attach(self, ctx: Any) -> "MossCall":
192
+ """
193
+ Bind a Moss call scope to a LiveKit ``JobContext``.
194
+
195
+ Idempotent on ``ctx.room.name`` - calling ``attach`` twice for the
196
+ same room returns scopes pointing at the same underlying call.
197
+ """
198
+ room = ctx.room
199
+ room_id: str = room.name
200
+ session_id: Optional[str] = None
201
+ job = getattr(ctx, "job", None)
202
+ if job is not None:
203
+ session_id = getattr(job, "id", None)
204
+
205
+ # `room.creation_time` is a UTC datetime; convert to ms-since-epoch.
206
+ creation_dt = room.creation_time
207
+ start_ms = int(creation_dt.timestamp() * 1000)
208
+
209
+ scope = self._inner.start_call(
210
+ room_id,
211
+ start_ms,
212
+ livekit_session_id=session_id,
213
+ )
214
+
215
+ call = MossCall(scope)
216
+
217
+ async def _on_shutdown(reason: Optional[str] = None) -> None:
218
+ await asyncio.to_thread(scope.end)
219
+
220
+ ctx.add_shutdown_callback(_on_shutdown)
221
+ return call
222
+
223
+ # -- Text-mode queries --------------------------------------------
224
+
225
+ async def query(
226
+ self,
227
+ name: str,
228
+ query: str,
229
+ *,
230
+ options: Optional[QueryOptions] = None,
231
+ ) -> SearchResult:
232
+ """
233
+ Run a text-mode query.
234
+
235
+ For voice agents serving a LiveKit room, prefer
236
+ :meth:`attach` + :meth:`MossCall.query` instead.
237
+ """
238
+ top_k = getattr(options, "top_k", None) or 5
239
+ alpha = getattr(options, "alpha", None) or 0.8
240
+ embedding = getattr(options, "embedding", None)
241
+ filter = getattr(options, "filter", None)
242
+
243
+ if embedding is None:
244
+ return await asyncio.to_thread(
245
+ self._inner.query_text,
246
+ name,
247
+ query,
248
+ top_k,
249
+ alpha,
250
+ filter,
251
+ )
252
+ return await asyncio.to_thread(
253
+ self._inner.query,
254
+ name,
255
+ query,
256
+ list(embedding),
257
+ top_k,
258
+ alpha,
259
+ filter,
260
+ )
261
+
262
+ async def query_multi_index(
263
+ self,
264
+ names: List[str],
265
+ query: str,
266
+ *,
267
+ options: Optional[QueryOptions] = None,
268
+ ) -> SearchResult:
269
+ """
270
+ Text-mode multi-index search. ``options.alpha`` is ignored
271
+ (multi-index search is embedding-only).
272
+ """
273
+ top_k = getattr(options, "top_k", None) or 10
274
+ embedding = getattr(options, "embedding", None)
275
+ filter = getattr(options, "filter", None)
276
+
277
+ if embedding is None:
278
+ return await asyncio.to_thread(
279
+ self._inner.query_multi_index_text,
280
+ names,
281
+ query,
282
+ top_k,
283
+ filter,
284
+ )
285
+ return await asyncio.to_thread(
286
+ self._inner.query_multi_index,
287
+ names,
288
+ query,
289
+ list(embedding),
290
+ top_k,
291
+ filter,
292
+ )
293
+
294
+ # -- Internal ------------------------------------------------------
295
+
296
+ def _resolve_model_id(
297
+ self, docs: List[DocumentInfo], model_id: Optional[str]
298
+ ) -> str:
299
+ if model_id is not None:
300
+ return model_id
301
+ has_embeddings = any(
302
+ getattr(doc, "embedding", None) is not None for doc in docs
303
+ )
304
+ return "custom" if has_embeddings else self.DEFAULT_MODEL_ID
305
+
306
+
307
+ class MossCall:
308
+ """
309
+ Per-call handle returned by :meth:`MossAgent.attach`.
310
+ """
311
+
312
+ def __init__(self, scope: Any) -> None:
313
+ self._scope = scope
314
+
315
+ @property
316
+ def call_id(self) -> str:
317
+ return self._scope.call_id
318
+
319
+ @property
320
+ def livekit_room_id(self) -> str:
321
+ return self._scope.livekit_room_id
322
+
323
+ @property
324
+ def livekit_session_id(self) -> Optional[str]:
325
+ return self._scope.livekit_session_id
326
+
327
+ @property
328
+ def start_at_unix_ms(self) -> int:
329
+ return self._scope.start_at_unix_ms
330
+
331
+ @property
332
+ def has_ended(self) -> bool:
333
+ return self._scope.has_ended
334
+
335
+ async def query(
336
+ self,
337
+ name: str,
338
+ query: str,
339
+ options: Optional[QueryOptions] = None,
340
+ ) -> SearchResult:
341
+ """
342
+ Run a tagged query against a loaded index.
343
+
344
+ Matches the semantics of ``moss.MossClient.query``: if
345
+ ``options.embedding`` is provided we use it directly, otherwise
346
+ we embed the query text using the index's own model.
347
+ """
348
+ top_k = getattr(options, "top_k", None) or 5
349
+ alpha = getattr(options, "alpha", None) or 0.8
350
+ embedding = getattr(options, "embedding", None)
351
+ filter = getattr(options, "filter", None)
352
+
353
+ if embedding is None:
354
+ return await asyncio.to_thread(
355
+ self._scope.query_text, name, query, top_k, alpha, filter
356
+ )
357
+ return await asyncio.to_thread(
358
+ self._scope.query,
359
+ name,
360
+ query,
361
+ list(embedding),
362
+ top_k,
363
+ alpha,
364
+ filter,
365
+ )
366
+
367
+ async def query_multi_index(
368
+ self,
369
+ names: List[str],
370
+ query: str,
371
+ options: Optional[QueryOptions] = None,
372
+ ) -> SearchResult:
373
+ """
374
+ Search across multiple loaded indexes. ``alpha`` is ignored
375
+ (multi-index search is embedding-only - see the underlying
376
+ ``moss.MossClient.query_multi_index`` docs for why).
377
+ """
378
+ top_k = getattr(options, "top_k", None) or 10
379
+ embedding = getattr(options, "embedding", None)
380
+ filter = getattr(options, "filter", None)
381
+
382
+ if embedding is None:
383
+ return await asyncio.to_thread(
384
+ self._scope.query_multi_index_text, names, query, top_k, filter
385
+ )
386
+ return await asyncio.to_thread(
387
+ self._scope.query_multi_index,
388
+ names,
389
+ query,
390
+ list(embedding),
391
+ top_k,
392
+ filter,
393
+ )
394
+
395
+ async def end(self) -> None:
396
+ """
397
+ End the call scope explicitly. Idempotent.
398
+
399
+ Normally you don't need to call this - ``attach`` registers a
400
+ shutdown callback on the LiveKit ``JobContext`` that calls
401
+ ``end`` for you when the room tears down. Call it manually only
402
+ if you need the scope closed before the room ends.
403
+ """
404
+ await asyncio.to_thread(self._scope.end)
File without changes
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: moss-agent
3
+ Version: 1.0.0
4
+ Summary: Moss runtime for LiveKit voice agents - a hot index cache shared across rooms and a one-line attach per call.
5
+ Author-email: "InferEdge Inc." <contact@usemoss.dev>
6
+ Project-URL: Homepage, https://github.com/usemoss/moss-samples
7
+ Project-URL: Repository, https://github.com/usemoss/moss-samples
8
+ Project-URL: Documentation, https://docs.usemoss.dev/
9
+ Keywords: livekit,voice-agent,semantic-search,moss,usemoss
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE.txt
22
+ Requires-Dist: typing-extensions>=4.0.0
23
+ Requires-Dist: inferedge-moss-core==0.12.0
24
+ Provides-Extra: livekit
25
+ Requires-Dist: livekit-agents>=0.10.0; extra == "livekit"
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8.4.2; extra == "dev"
28
+ Requires-Dist: pytest-asyncio>=1.2.0; extra == "dev"
29
+ Requires-Dist: black>=25.9.0; extra == "dev"
30
+ Requires-Dist: isort>=7.0.0; extra == "dev"
31
+ Requires-Dist: mypy>=1.18.2; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # moss-agent
35
+
36
+ Moss runtime for LiveKit voice agents - a hot index cache shared across rooms and a one-line attach per call.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install moss-agent
42
+ ```
43
+
44
+ ## Use - voice (LiveKit)
45
+
46
+ ```python
47
+ from livekit.agents import AgentServer, JobContext, JobProcess
48
+ from moss_agent import MossAgent
49
+
50
+ server = AgentServer()
51
+
52
+
53
+ def prewarm(proc: JobProcess) -> None:
54
+ agent = MossAgent(
55
+ project_id="...",
56
+ project_key="...",
57
+ )
58
+ import asyncio
59
+ asyncio.run(agent.load_indexes(["product_catalog", "faq", "policies"]))
60
+ proc.userdata["moss_agent"] = agent
61
+
62
+
63
+ server.setup_fnc = prewarm
64
+
65
+
66
+ @server.rtc_session(agent_name="my-agent")
67
+ async def handle_visit(ctx: JobContext) -> None:
68
+ await ctx.connect()
69
+
70
+ agent: MossAgent = ctx.proc.userdata["moss_agent"]
71
+ call = agent.attach(ctx)
72
+
73
+ results = await call.query("product_catalog", user_question)
74
+ # ... your existing agent loop ...
75
+ ```
76
+
77
+ ## Use - text (HTTP / chat / non-LiveKit)
78
+
79
+ ```python
80
+ from moss_agent import MossAgent
81
+
82
+ # Module level - reuse across requests
83
+ agent = MossAgent(project_id="...", project_key="...")
84
+ await agent.load_indexes(["product_catalog"])
85
+
86
+
87
+ # In your HTTP / chat handler
88
+ async def handle_message(req):
89
+ results = await agent.query("product_catalog", req.user_message)
90
+ # ... feed results to your model ...
91
+ ```
92
+
93
+ ## API surface
94
+
95
+ - `MossAgent(project_id, project_key)` - process-wide instance. Build once, share across every room (voice) or every request (text).
96
+ - **Voice:** `MossAgent.attach(ctx) -> MossCall` binds a Moss call scope to a LiveKit `JobContext`. Idempotent on `ctx.room.name`. `MossCall.query(name, query, options=None)` and `MossCall.query_multi_index(names, query, options=None)` are the call-scoped query surface.
97
+ - **Text:** `MossAgent.query(name, query, options=None)` and `MossAgent.query_multi_index(names, query, options=None)`.
98
+ - Full index CRUD (`create_index`, `add_docs`, `delete_docs`, `delete_index`, `list_indexes`, `get_index`, `get_docs`, `get_job_status`) and cache lifecycle (`load_index`, `load_indexes`, `unload_index`, `unload_indexes`) on `MossAgent`.
99
+
100
+ ## Requirements
101
+
102
+ - Python 3.10+
103
+ - `inferedge-moss-core == 0.12.0`
104
+ - `livekit-agents >= 0.10.0` (only required if you call `attach()`; install via the `[livekit]` extra)
105
+
106
+ ## License
107
+
108
+ Proprietary. See `LICENSE.txt`.
@@ -0,0 +1,12 @@
1
+ LICENSE.txt
2
+ README.md
3
+ pyproject.toml
4
+ src/moss_agent/__init__.py
5
+ src/moss_agent/client.py
6
+ src/moss_agent/py.typed
7
+ src/moss_agent.egg-info/PKG-INFO
8
+ src/moss_agent.egg-info/SOURCES.txt
9
+ src/moss_agent.egg-info/dependency_links.txt
10
+ src/moss_agent.egg-info/requires.txt
11
+ src/moss_agent.egg-info/top_level.txt
12
+ tests/test_client.py
@@ -0,0 +1,12 @@
1
+ typing-extensions>=4.0.0
2
+ inferedge-moss-core==0.12.0
3
+
4
+ [dev]
5
+ pytest>=8.4.2
6
+ pytest-asyncio>=1.2.0
7
+ black>=25.9.0
8
+ isort>=7.0.0
9
+ mypy>=1.18.2
10
+
11
+ [livekit]
12
+ livekit-agents>=0.10.0
@@ -0,0 +1 @@
1
+ moss_agent
@@ -0,0 +1,304 @@
1
+ """
2
+ Unit tests for the ``moss_agent`` Python wrapper layer.
3
+
4
+ Scope: behaviors implemented in Python - constructor argument forwarding,
5
+ LiveKit ``JobContext`` unpacking in ``attach``, query-method routing
6
+ between embedding and text variants, asyncio bridging via
7
+ ``asyncio.to_thread``. Rust-side semantics (idempotency, lifecycle,
8
+ concurrency, text-mode delay floor) are covered by the moss_core test
9
+ suite.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import datetime
15
+ from types import SimpleNamespace
16
+ from unittest.mock import MagicMock, patch
17
+
18
+ import pytest
19
+
20
+
21
+ # ---------- Helpers ----------
22
+
23
+ def _make_fake_ctx(
24
+ *,
25
+ room_name: str = "rm_1",
26
+ job_id: str | None = "job_1",
27
+ creation_time: datetime.datetime | None = None,
28
+ ):
29
+ """Build a minimal stand-in for ``livekit.agents.JobContext``."""
30
+ if creation_time is None:
31
+ creation_time = datetime.datetime(
32
+ 2024, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc
33
+ )
34
+ callbacks: list = []
35
+
36
+ ctx = MagicMock()
37
+ ctx.room.name = room_name
38
+ ctx.room.creation_time = creation_time
39
+ if job_id is None:
40
+ ctx.job = None
41
+ else:
42
+ ctx.job.id = job_id
43
+ ctx.add_shutdown_callback = callbacks.append
44
+ return ctx, callbacks
45
+
46
+
47
+ # ---------- MossAgent constructor ----------
48
+
49
+ @patch("moss_agent.client._Agent")
50
+ def test_constructor_forwards_credentials_only(
51
+ mock_agent_class: MagicMock,
52
+ ) -> None:
53
+ from moss_agent import MossAgent
54
+
55
+ MossAgent("p", "k")
56
+ mock_agent_class.assert_called_once_with(
57
+ "p", "k", base_url=None, client_id=None
58
+ )
59
+
60
+
61
+ @patch("moss_agent.client._Agent")
62
+ def test_constructor_forwards_base_url_and_client_id(
63
+ mock_agent_class: MagicMock,
64
+ ) -> None:
65
+ from moss_agent import MossAgent
66
+
67
+ MossAgent("p", "k", base_url="http://x", client_id="cid")
68
+ mock_agent_class.assert_called_once_with(
69
+ "p", "k", base_url="http://x", client_id="cid"
70
+ )
71
+
72
+
73
+ @patch("moss_agent.client._Agent")
74
+ def test_client_id_property_reads_from_rust(mock_agent_class: MagicMock) -> None:
75
+ mock_agent_class.return_value.client_id.return_value = "cid-xyz"
76
+
77
+ from moss_agent import MossAgent
78
+
79
+ assert MossAgent("p", "k").client_id == "cid-xyz"
80
+
81
+
82
+ # ---------- attach() ctx unpacking ----------
83
+
84
+ @patch("moss_agent.client._Agent")
85
+ def test_attach_converts_creation_time_to_unix_ms(
86
+ mock_agent_class: MagicMock,
87
+ ) -> None:
88
+ mock_agent_class.return_value.start_call.return_value = MagicMock()
89
+
90
+ from moss_agent import MossAgent
91
+
92
+ creation = datetime.datetime(
93
+ 2024, 6, 15, 12, 0, 0, tzinfo=datetime.timezone.utc
94
+ )
95
+ expected_ms = int(creation.timestamp() * 1000)
96
+
97
+ ctx, _ = _make_fake_ctx(creation_time=creation)
98
+ MossAgent("p", "k").attach(ctx)
99
+
100
+ mock_agent_class.return_value.start_call.assert_called_once_with(
101
+ "rm_1",
102
+ expected_ms,
103
+ livekit_session_id="job_1",
104
+ )
105
+
106
+
107
+ @patch("moss_agent.client._Agent")
108
+ def test_attach_handles_missing_job(mock_agent_class: MagicMock) -> None:
109
+ mock_agent_class.return_value.start_call.return_value = MagicMock()
110
+
111
+ from moss_agent import MossAgent
112
+
113
+ ctx, _ = _make_fake_ctx(job_id=None)
114
+ MossAgent("p", "k").attach(ctx)
115
+
116
+ _, kwargs = mock_agent_class.return_value.start_call.call_args
117
+ assert kwargs["livekit_session_id"] is None
118
+
119
+
120
+ @patch("moss_agent.client._Agent")
121
+ def test_attach_registers_shutdown_callback(
122
+ mock_agent_class: MagicMock,
123
+ ) -> None:
124
+ mock_agent_class.return_value.start_call.return_value = MagicMock()
125
+
126
+ from moss_agent import MossAgent
127
+
128
+ ctx, callbacks = _make_fake_ctx()
129
+ MossAgent("p", "k").attach(ctx)
130
+
131
+ assert len(callbacks) == 1
132
+
133
+
134
+ @pytest.mark.asyncio
135
+ @patch("moss_agent.client._Agent")
136
+ async def test_attach_shutdown_callback_invokes_scope_end(
137
+ mock_agent_class: MagicMock,
138
+ ) -> None:
139
+ scope = MagicMock()
140
+ mock_agent_class.return_value.start_call.return_value = scope
141
+
142
+ from moss_agent import MossAgent
143
+
144
+ ctx, callbacks = _make_fake_ctx()
145
+ MossAgent("p", "k").attach(ctx)
146
+
147
+ await callbacks[0]("test reason")
148
+ scope.end.assert_called_once()
149
+
150
+
151
+ # ---------- MossAgent text-mode query routing ----------
152
+
153
+ @pytest.mark.asyncio
154
+ @patch("moss_agent.client._Agent")
155
+ async def test_text_query_without_embedding_uses_query_text(
156
+ mock_agent_class: MagicMock,
157
+ ) -> None:
158
+ inner = mock_agent_class.return_value
159
+
160
+ from moss_agent import MossAgent
161
+
162
+ await MossAgent("p", "k").query("kb1", "hello")
163
+ inner.query_text.assert_called_once_with("kb1", "hello", 5, 0.8, None)
164
+ inner.query.assert_not_called()
165
+
166
+
167
+ @pytest.mark.asyncio
168
+ @patch("moss_agent.client._Agent")
169
+ async def test_text_query_with_embedding_uses_embedding_path(
170
+ mock_agent_class: MagicMock,
171
+ ) -> None:
172
+ inner = mock_agent_class.return_value
173
+ options = SimpleNamespace(
174
+ top_k=10, alpha=0.7, embedding=[0.1, 0.2, 0.3], filter=None
175
+ )
176
+
177
+ from moss_agent import MossAgent
178
+
179
+ await MossAgent("p", "k").query("kb1", "hello", options=options)
180
+ inner.query.assert_called_once_with(
181
+ "kb1", "hello", [0.1, 0.2, 0.3], 10, 0.7, None
182
+ )
183
+ inner.query_text.assert_not_called()
184
+
185
+
186
+ @pytest.mark.asyncio
187
+ @patch("moss_agent.client._Agent")
188
+ async def test_text_multi_index_without_embedding_uses_text_variant(
189
+ mock_agent_class: MagicMock,
190
+ ) -> None:
191
+ inner = mock_agent_class.return_value
192
+
193
+ from moss_agent import MossAgent
194
+
195
+ await MossAgent("p", "k").query_multi_index(["kb1", "kb2"], "hello")
196
+ inner.query_multi_index_text.assert_called_once_with(
197
+ ["kb1", "kb2"], "hello", 10, None
198
+ )
199
+ inner.query_multi_index.assert_not_called()
200
+
201
+
202
+ @pytest.mark.asyncio
203
+ @patch("moss_agent.client._Agent")
204
+ async def test_text_multi_index_with_embedding_uses_embedding_variant(
205
+ mock_agent_class: MagicMock,
206
+ ) -> None:
207
+ inner = mock_agent_class.return_value
208
+ options = SimpleNamespace(top_k=5, embedding=[0.1, 0.2], filter=None)
209
+
210
+ from moss_agent import MossAgent
211
+
212
+ await MossAgent("p", "k").query_multi_index(
213
+ ["kb1", "kb2"], "hello", options=options
214
+ )
215
+ inner.query_multi_index.assert_called_once_with(
216
+ ["kb1", "kb2"], "hello", [0.1, 0.2], 5, None
217
+ )
218
+ inner.query_multi_index_text.assert_not_called()
219
+
220
+
221
+ # ---------- MossCall accessors ----------
222
+
223
+ def test_moss_call_passthrough_properties() -> None:
224
+ scope = MagicMock()
225
+ scope.call_id = "cid"
226
+ scope.livekit_room_id = "rm_1"
227
+ scope.livekit_session_id = "sess"
228
+ scope.start_at_unix_ms = 12345
229
+ scope.has_ended = False
230
+
231
+ from moss_agent import MossCall
232
+
233
+ call = MossCall(scope)
234
+ assert call.call_id == "cid"
235
+ assert call.livekit_room_id == "rm_1"
236
+ assert call.livekit_session_id == "sess"
237
+ assert call.start_at_unix_ms == 12345
238
+ assert call.has_ended is False
239
+
240
+
241
+ # ---------- MossCall.query routing (voice) ----------
242
+
243
+ @pytest.mark.asyncio
244
+ async def test_call_query_without_embedding_uses_query_text() -> None:
245
+ scope = MagicMock()
246
+
247
+ from moss_agent import MossCall
248
+
249
+ await MossCall(scope).query("kb1", "hello")
250
+ scope.query_text.assert_called_once_with("kb1", "hello", 5, 0.8, None)
251
+ scope.query.assert_not_called()
252
+
253
+
254
+ @pytest.mark.asyncio
255
+ async def test_call_query_with_embedding_uses_embedding_path() -> None:
256
+ scope = MagicMock()
257
+ options = SimpleNamespace(
258
+ top_k=10, alpha=0.7, embedding=[0.1, 0.2, 0.3], filter=None
259
+ )
260
+
261
+ from moss_agent import MossCall
262
+
263
+ await MossCall(scope).query("kb1", "hello", options)
264
+ scope.query.assert_called_once_with(
265
+ "kb1", "hello", [0.1, 0.2, 0.3], 10, 0.7, None
266
+ )
267
+ scope.query_text.assert_not_called()
268
+
269
+
270
+ @pytest.mark.asyncio
271
+ async def test_call_query_multi_index_without_embedding_uses_text_variant() -> None:
272
+ scope = MagicMock()
273
+
274
+ from moss_agent import MossCall
275
+
276
+ await MossCall(scope).query_multi_index(["kb1", "kb2"], "hello")
277
+ scope.query_multi_index_text.assert_called_once_with(
278
+ ["kb1", "kb2"], "hello", 10, None
279
+ )
280
+ scope.query_multi_index.assert_not_called()
281
+
282
+
283
+ @pytest.mark.asyncio
284
+ async def test_call_query_multi_index_with_embedding_uses_embedding_variant() -> None:
285
+ scope = MagicMock()
286
+ options = SimpleNamespace(top_k=5, embedding=[0.1, 0.2], filter=None)
287
+
288
+ from moss_agent import MossCall
289
+
290
+ await MossCall(scope).query_multi_index(["kb1", "kb2"], "hello", options)
291
+ scope.query_multi_index.assert_called_once_with(
292
+ ["kb1", "kb2"], "hello", [0.1, 0.2], 5, None
293
+ )
294
+ scope.query_multi_index_text.assert_not_called()
295
+
296
+
297
+ @pytest.mark.asyncio
298
+ async def test_call_end_invokes_scope_end() -> None:
299
+ scope = MagicMock()
300
+
301
+ from moss_agent import MossCall
302
+
303
+ await MossCall(scope).end()
304
+ scope.end.assert_called_once()