diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0064244d..eaa05b1e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,14 +21,10 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - # Pin ruff: 0.16.0 (released 2026-07-23) turned on a large new rule - # surface that flags ~460 pre-existing findings across the repo and - # breaks CI on every PR. Pinned to the last green version; bump only - # once those findings are addressed (see python-sdk-test for the twin). - pip install ruff==0.15.22 mypy + pip install "ruff~=0.16.7" mypy if [ -f examples/python/requirements.txt ]; then pip install -r examples/python/requirements.txt; fi - name: Lint with ruff - run: ruff check . --extend-exclude '**/*.ipynb' + run: ruff check . --extend-exclude '*.ipynb' - name: Type check with mypy run: mypy examples/python apps/pipecat-moss apps/livekit-moss-vercel --ignore-missing-imports --exclude "apps/pipecat-moss/hume-ollama-local/create_index.py" @@ -95,7 +91,7 @@ jobs: - name: Lint with ruff working-directory: sdks/python/sdk run: | - pip install ruff==0.15.22 # pinned; see python-lint job for why (ruff 0.16.0 regression) + pip install "ruff~=0.16.7" ruff check src/ tests/ - name: Type check with mypy working-directory: sdks/python/sdk diff --git a/.gitignore b/.gitignore index b5028c58..379012e6 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ skills-lock.json # Swift / SwiftPM build artifacts .build/ + +# Python SDK setuptools build output +sdks/python/sdk/build/ diff --git a/apps/agora-custom-llm-moss/server/src/llm.py b/apps/agora-custom-llm-moss/server/src/llm.py index a49dfd4d..28a7e6c1 100644 --- a/apps/agora-custom-llm-moss/server/src/llm.py +++ b/apps/agora-custom-llm-moss/server/src/llm.py @@ -17,7 +17,7 @@ import uuid from contextlib import asynccontextmanager from pathlib import Path -from typing import Any, Dict, List, Optional, Union +from typing import Any from dotenv import load_dotenv from fastapi import FastAPI, Header, HTTPException @@ -54,36 +54,36 @@ class TextContent(BaseModel): class SystemMessage(BaseModel): role: str = "system" - content: Union[str, List[str]] + content: str | list[str] class UserMessage(BaseModel): role: str = "user" - content: Union[str, List[Union[TextContent, Dict]]] + content: str | list[TextContent | dict] class AssistantMessage(BaseModel): role: str = "assistant" - content: Union[str, List[TextContent], None] = None - tool_calls: Optional[List[Dict]] = None + content: str | list[TextContent] | None = None + tool_calls: list[dict] | None = None class ToolMessage(BaseModel): role: str = "tool" - content: Union[str, List[str]] + content: str | list[str] tool_call_id: str class ChatCompletionRequest(BaseModel): - model: Optional[str] = None - messages: List[Union[SystemMessage, UserMessage, AssistantMessage, ToolMessage]] + model: str | None = None + messages: list[SystemMessage | UserMessage | AssistantMessage | ToolMessage] stream: bool = True - stream_options: Optional[Dict] = None - temperature: Optional[float] = None - max_tokens: Optional[int] = None - tools: Optional[List[Dict]] = None - tool_choice: Optional[Union[str, Dict]] = None - response_format: Optional[Dict] = None + stream_options: dict | None = None + temperature: float | None = None + max_tokens: int | None = None + tools: list[dict] | None = None + tool_choice: str | dict | None = None + response_format: dict | None = None def load_server_env(server_dir: Path | None = None) -> None: @@ -164,7 +164,7 @@ async def open_moss(): return session -def require_bearer(authorization: Optional[str], mock: bool) -> None: +def require_bearer(authorization: str | None, mock: bool) -> None: if mock: return if not authorization or not authorization.lower().startswith("bearer "): @@ -320,7 +320,7 @@ async def lifespan(_app: FastAPI): @app.post("/chat/completions") async def chat_completions( request: ChatCompletionRequest, - authorization: Optional[str] = Header(None, alias="Authorization"), + authorization: str | None = Header(None, alias="Authorization"), ): require_bearer(authorization, mock=mock) if not request.stream: diff --git a/apps/agora-custom-llm-moss/server/src/server.py b/apps/agora-custom-llm-moss/server/src/server.py index b89f0f96..9624f6f7 100644 --- a/apps/agora-custom-llm-moss/server/src/server.py +++ b/apps/agora-custom-llm-moss/server/src/server.py @@ -9,7 +9,6 @@ import os from fastapi import FastAPI - from llm import create_app, load_server_env load_server_env() diff --git a/apps/docker/python/main.py b/apps/docker/python/main.py index 363177af..7c212f94 100644 --- a/apps/docker/python/main.py +++ b/apps/docker/python/main.py @@ -12,6 +12,7 @@ import asyncio import os + from dotenv import load_dotenv from moss import MossClient, QueryOptions diff --git a/apps/moss-llamaindex/backend/main.py b/apps/moss-llamaindex/backend/main.py index db0c03d5..ee8e5d13 100644 --- a/apps/moss-llamaindex/backend/main.py +++ b/apps/moss-llamaindex/backend/main.py @@ -11,22 +11,22 @@ """ import asyncio +import json import os import tempfile import time import uuid -import json +from collections.abc import AsyncGenerator from contextlib import asynccontextmanager -from typing import AsyncGenerator -from fastapi import FastAPI, APIRouter, File, UploadFile, HTTPException +from dotenv import load_dotenv +from fastapi import APIRouter, FastAPI, File, HTTPException, UploadFile from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import StreamingResponse -from pydantic import BaseModel -from dotenv import load_dotenv -from moss import MossClient, DocumentInfo, QueryOptions from liteparse import LiteParse +from moss import DocumentInfo, MossClient, QueryOptions from nltk.tokenize import sent_tokenize +from pydantic import BaseModel load_dotenv() diff --git a/apps/moss-llamaindex/backend/seed_index.py b/apps/moss-llamaindex/backend/seed_index.py index 000eac2d..8777d203 100644 --- a/apps/moss-llamaindex/backend/seed_index.py +++ b/apps/moss-llamaindex/backend/seed_index.py @@ -17,8 +17,8 @@ from pathlib import Path from dotenv import load_dotenv -from moss import MossClient, DocumentInfo from liteparse import LiteParse +from moss import DocumentInfo, MossClient from nltk.tokenize import sent_tokenize load_dotenv() diff --git a/apps/ten-moss/ruff.toml b/apps/ten-moss/ruff.toml index 4149b71b..73c15cdb 100644 --- a/apps/ten-moss/ruff.toml +++ b/apps/ten-moss/ruff.toml @@ -2,6 +2,8 @@ # see apps/ten-moss/README.md "Provenance"). Its runtime/agent modules ship with # star-imports and package re-exports upstream, which we don't restyle. Silence # those vendored-code lints; the Moss delta (extension.py, config.py) stays linted. +extend = "../../ruff.toml" + [lint.per-file-ignores] "tenapp/ten_packages/extension/main_python/__init__.py" = ["F401"] "tenapp/ten_packages/extension/main_python/agent/*.py" = ["F401", "F403", "F405", "F841"] diff --git a/apps/ten-moss/tenapp/ten_packages/extension/main_python/addon.py b/apps/ten-moss/tenapp/ten_packages/extension/main_python/addon.py index d7441c50..89a4bb79 100644 --- a/apps/ten-moss/tenapp/ten_packages/extension/main_python/addon.py +++ b/apps/ten-moss/tenapp/ten_packages/extension/main_python/addon.py @@ -5,8 +5,8 @@ # from ten_runtime import ( Addon, - register_addon_as_extension, TenEnv, + register_addon_as_extension, ) diff --git a/apps/ten-moss/tenapp/ten_packages/extension/main_python/agent/agent.py b/apps/ten-moss/tenapp/ten_packages/extension/main_python/agent/agent.py index 5a4b4e9b..dbc178a2 100644 --- a/apps/ten-moss/tenapp/ten_packages/extension/main_python/agent/agent.py +++ b/apps/ten-moss/tenapp/ten_packages/extension/main_python/agent/agent.py @@ -1,10 +1,13 @@ import asyncio import json -from typing import Awaitable, Callable, Optional -from .llm_exec import LLMExec -from ten_runtime import AsyncTenEnv, Cmd, CmdResult, Data, StatusCode +from collections.abc import Awaitable, Callable +from typing import Optional + from ten_ai_base.types import LLMToolMetadata +from ten_runtime import AsyncTenEnv, Cmd, CmdResult, Data, StatusCode + from .events import * +from .llm_exec import LLMExec class Agent: @@ -22,9 +25,9 @@ def __init__(self, ten_env: AsyncTenEnv): self._llm_queue: asyncio.Queue[LLMResponseEvent] = asyncio.Queue() # Current consumer tasks - self._asr_consumer: Optional[asyncio.Task] = None - self._llm_consumer: Optional[asyncio.Task] = None - self._llm_active_task: Optional[asyncio.Task] = ( + self._asr_consumer: asyncio.Task | None = None + self._llm_consumer: asyncio.Task | None = None + self._llm_active_task: asyncio.Task | None = ( None # currently running handler ) @@ -44,7 +47,7 @@ def __init__(self, ten_env: AsyncTenEnv): def on( self, event_type: AgentEvent, - handler: Callable[[AgentEvent], Awaitable] = None, + handler: Callable[[AgentEvent], Awaitable] | None = None, ): """ Register a callback for a given event type. diff --git a/apps/ten-moss/tenapp/ten_packages/extension/main_python/agent/decorators.py b/apps/ten-moss/tenapp/ten_packages/extension/main_python/agent/decorators.py index dbab6c62..0980efcf 100644 --- a/apps/ten-moss/tenapp/ten_packages/extension/main_python/agent/decorators.py +++ b/apps/ten-moss/tenapp/ten_packages/extension/main_python/agent/decorators.py @@ -10,7 +10,7 @@ async def on_asr(self, event: ASRResultEvent): ... """ def wrapper(func): - setattr(func, "_agent_event_type", event_type) + func._agent_event_type = event_type return func return wrapper diff --git a/apps/ten-moss/tenapp/ten_packages/extension/main_python/agent/events.py b/apps/ten-moss/tenapp/ten_packages/extension/main_python/agent/events.py index df61ec5c..b029adda 100644 --- a/apps/ten-moss/tenapp/ten_packages/extension/main_python/agent/events.py +++ b/apps/ten-moss/tenapp/ten_packages/extension/main_python/agent/events.py @@ -1,8 +1,8 @@ +from typing import Any, Literal + from pydantic import BaseModel -from typing import Literal, Union, Dict, Any from ten_ai_base.types import LLMToolMetadata - # ==== Base Event ==== @@ -49,7 +49,7 @@ class ASRResultEvent(AgentEventBase): name: Literal["asr_result"] = "asr_result" text: str final: bool - metadata: Dict[str, Any] + metadata: dict[str, Any] class LLMResponseEvent(AgentEventBase): @@ -64,10 +64,10 @@ class LLMResponseEvent(AgentEventBase): # ==== Unified Event Union ==== -AgentEvent = Union[ - UserJoinedEvent, - UserLeftEvent, - ToolRegisterEvent, - ASRResultEvent, - LLMResponseEvent, -] +AgentEvent = ( + UserJoinedEvent + | UserLeftEvent + | ToolRegisterEvent + | ASRResultEvent + | LLMResponseEvent +) diff --git a/apps/ten-moss/tenapp/ten_packages/extension/main_python/agent/llm_exec.py b/apps/ten-moss/tenapp/ten_packages/extension/main_python/agent/llm_exec.py index 5e57598d..6f2ecab3 100644 --- a/apps/ten-moss/tenapp/ten_packages/extension/main_python/agent/llm_exec.py +++ b/apps/ten-moss/tenapp/ten_packages/extension/main_python/agent/llm_exec.py @@ -6,7 +6,10 @@ import asyncio import json import traceback -from typing import Awaitable, Callable, Literal, Optional +import uuid +from collections.abc import Awaitable, Callable +from typing import Literal, Optional + from ten_ai_base.const import CMD_PROPERTY_RESULT from ten_ai_base.helper import AsyncQueue from ten_ai_base.struct import ( @@ -24,9 +27,9 @@ parse_llm_response, ) from ten_ai_base.types import LLMToolMetadata, LLMToolResult -from ..helper import _send_cmd, _send_cmd_ex from ten_runtime import AsyncTenEnv, Loc, StatusCode -import uuid + +from ..helper import _send_cmd, _send_cmd_ex class LLMExec: @@ -39,16 +42,10 @@ def __init__(self, ten_env: AsyncTenEnv): self.ten_env = ten_env self.input_queue = AsyncQueue() self.stopped = False - self.on_response: Optional[ - Callable[[AsyncTenEnv, str, str, bool], Awaitable[None]] - ] = None - self.on_reasoning_response: Optional[ - Callable[[AsyncTenEnv, str, str, bool], Awaitable[None]] - ] = None - self.on_tool_call: Optional[ - Callable[[AsyncTenEnv, LLMToolMetadata], Awaitable[None]] - ] = None - self.current_task: Optional[asyncio.Task] = None + self.on_response: Callable[[AsyncTenEnv, str, str, bool], Awaitable[None]] | None = None + self.on_reasoning_response: Callable[[AsyncTenEnv, str, str, bool], Awaitable[None]] | None = None + self.on_tool_call: Callable[[AsyncTenEnv, LLMToolMetadata], Awaitable[None]] | None = None + self.current_task: asyncio.Task | None = None self.loop = asyncio.get_event_loop() self.loop.create_task(self._process_input_queue()) self.available_tools: list[LLMToolMetadata] = [] @@ -57,7 +54,7 @@ def __init__(self, ten_env: AsyncTenEnv): asyncio.Lock() ) # Lock to ensure thread-safe access self.contexts: list[LLMMessage] = [] - self.current_request_id: Optional[str] = None + self.current_request_id: str | None = None self.current_text = None async def queue_input(self, item: str) -> None: @@ -169,14 +166,17 @@ async def _send_to_llm( await self._queue_context(ten_env, new_message) async for cmd_result, _ in response: - if cmd_result and cmd_result.is_final() is False: - if cmd_result.get_status_code() == StatusCode.OK: - response_json, _ = cmd_result.get_property_to_json(None) - ten_env.log_info( - f"_send_to_llm: response_json {response_json}" - ) - completion = parse_llm_response(response_json) - await self._handle_llm_response(completion) + if ( + cmd_result + and cmd_result.is_final() is False + and cmd_result.get_status_code() == StatusCode.OK + ): + response_json, _ = cmd_result.get_property_to_json(None) + ten_env.log_info( + f"_send_to_llm: response_json {response_json}" + ) + completion = parse_llm_response(response_json) + await self._handle_llm_response(completion) async def _handle_llm_response(self, llm_output: LLMResponse | None): self.ten_env.log_info(f"_handle_llm_response: {llm_output}") diff --git a/apps/ten-moss/tenapp/ten_packages/extension/main_python/extension.py b/apps/ten-moss/tenapp/ten_packages/extension/main_python/extension.py index 1488031b..ab5b765d 100644 --- a/apps/ten-moss/tenapp/ten_packages/extension/main_python/extension.py +++ b/apps/ten-moss/tenapp/ten_packages/extension/main_python/extension.py @@ -1,8 +1,11 @@ import json import time +import uuid from typing import Literal -from .agent.decorators import agent_event_handler +from ten_ai_base.const import CMD_PROPERTY_RESULT +from ten_ai_base.types import LLMToolMetadata, LLMToolMetadataParameter +from ten_moss import MossSessionManager from ten_runtime import ( AsyncExtension, AsyncTenEnv, @@ -13,6 +16,7 @@ ) from .agent.agent import Agent +from .agent.decorators import agent_event_handler from .agent.events import ( ASRResultEvent, LLMResponseEvent, @@ -20,14 +24,8 @@ UserJoinedEvent, UserLeftEvent, ) -from .helper import _send_cmd, _send_data, parse_sentences from .config import MainControlConfig - -from ten_moss import MossSessionManager -from ten_ai_base.const import CMD_PROPERTY_RESULT -from ten_ai_base.types import LLMToolMetadata, LLMToolMetadataParameter - -import uuid +from .helper import _send_cmd, _send_data, parse_sentences SEARCH_KNOWLEDGE_BASE = "search_knowledge_base" MAX_MOSS_TOOL_CALLS = 2 @@ -219,7 +217,7 @@ async def _on_tool_call(self, cmd: Cmd) -> None: try: raw, _ = cmd.get_property_to_json(None) payload = json.loads(raw) if raw else {} - except Exception as exc: # noqa: BLE001 + except Exception as exc: self.ten_env.log_error( f"[MainControlExtension] tool_call payload unreadable: {exc}" ) @@ -280,7 +278,7 @@ async def _query_moss(self, user_text: str) -> str: f"time_taken_ms={sdk_ms} (wall_clock={took_ms:.0f}ms)" ) return context - except Exception as exc: # noqa: BLE001 + except Exception as exc: self.ten_env.log_error( f"[MainControlExtension] Moss grounding failed: {exc}" ) diff --git a/apps/ten-moss/tenapp/ten_packages/extension/main_python/helper.py b/apps/ten-moss/tenapp/ten_packages/extension/main_python/helper.py index 29ec69ea..e8d032a4 100644 --- a/apps/ten-moss/tenapp/ten_packages/extension/main_python/helper.py +++ b/apps/ten-moss/tenapp/ten_packages/extension/main_python/helper.py @@ -5,14 +5,14 @@ # import json -from typing import Any, AsyncGenerator, Optional +from collections.abc import AsyncGenerator +from typing import Any + from ten_runtime import AsyncTenEnv, Cmd, CmdResult, Data, Loc, TenError def is_punctuation(char): - if char in [",", ",", ".", "。", "?", "?", "!", "!"]: - return True - return False + return char in [",", ",", ".", "。", "?", "?", "!", "!"] def parse_sentences(sentence_fragment, content): @@ -33,7 +33,7 @@ def parse_sentences(sentence_fragment, content): async def _send_cmd( ten_env: AsyncTenEnv, cmd_name: str, dest: str, payload: Any = None -) -> tuple[Optional[CmdResult], Optional[TenError]]: +) -> tuple[CmdResult | None, TenError | None]: """ Convenient method to send a command with a payload within app/graph w/o need to create a connection. Note: extension using this approach will contain logics that are meaningful for this graph only, @@ -52,7 +52,7 @@ async def _send_cmd( async def _send_cmd_ex( ten_env: AsyncTenEnv, cmd_name: str, dest: str, payload: Any = None -) -> AsyncGenerator[tuple[Optional[CmdResult], Optional[TenError]], None]: +) -> AsyncGenerator[tuple[CmdResult | None, TenError | None], None]: """Convenient method to send a command with a payload within app/graph w/o need to create a connection. Note: extension using this approach will contain logics that are meaningful for this graph only, as it will assume the target extension already exists in the graph. @@ -73,7 +73,7 @@ async def _send_cmd_ex( async def _send_data( ten_env: AsyncTenEnv, data_name: str, dest: str, payload: Any = None -) -> Optional[TenError]: +) -> TenError | None: """Convenient method to send data with a payload within app/graph w/o need to create a connection. Note: extension using this approach will contain logics that are meaningful for this graph only, as it will assume the target extension already exists in the graph. diff --git a/benchmarks/bench_chroma.py b/benchmarks/bench_chroma.py index 479a70d5..59679964 100644 --- a/benchmarks/bench_chroma.py +++ b/benchmarks/bench_chroma.py @@ -5,11 +5,10 @@ """ -from dotenv import load_dotenv - from corpus import get_documents, get_queries +from dotenv import load_dotenv from embedding import EmbeddingClient -from stats import Timer, BenchmarkResult +from stats import BenchmarkResult, Timer load_dotenv() diff --git a/benchmarks/bench_moss.py b/benchmarks/bench_moss.py index 2b994f65..4bc4bb1d 100644 --- a/benchmarks/bench_moss.py +++ b/benchmarks/bench_moss.py @@ -7,10 +7,9 @@ import asyncio import os -from dotenv import load_dotenv - from corpus import get_documents, get_queries -from stats import Timer, BenchmarkResult +from dotenv import load_dotenv +from stats import BenchmarkResult, Timer load_dotenv() @@ -21,7 +20,7 @@ async def run_async(): - from moss import MossClient, DocumentInfo, QueryOptions + from moss import DocumentInfo, MossClient, QueryOptions queries = get_queries() diff --git a/benchmarks/bench_pinecone.py b/benchmarks/bench_pinecone.py index a62b5546..afd10c3f 100644 --- a/benchmarks/bench_pinecone.py +++ b/benchmarks/bench_pinecone.py @@ -8,11 +8,10 @@ import os import time -from dotenv import load_dotenv - from corpus import get_documents, get_queries +from dotenv import load_dotenv from embedding import EmbeddingClient -from stats import Timer, BenchmarkResult +from stats import BenchmarkResult, Timer load_dotenv() diff --git a/benchmarks/bench_qdrant.py b/benchmarks/bench_qdrant.py index aafb000a..44039a71 100644 --- a/benchmarks/bench_qdrant.py +++ b/benchmarks/bench_qdrant.py @@ -8,11 +8,10 @@ import os -from dotenv import load_dotenv - from corpus import get_documents, get_queries +from dotenv import load_dotenv from embedding import EmbeddingClient -from stats import Timer, BenchmarkResult +from stats import BenchmarkResult, Timer load_dotenv() diff --git a/benchmarks/embedding.py b/benchmarks/embedding.py index 0ad747ad..970a9ff2 100644 --- a/benchmarks/embedding.py +++ b/benchmarks/embedding.py @@ -11,6 +11,7 @@ """ import os + import requests from openai import OpenAI diff --git a/benchmarks/run_all.py b/benchmarks/run_all.py old mode 100644 new mode 100755 index ed5bf360..5f904a84 --- a/benchmarks/run_all.py +++ b/benchmarks/run_all.py @@ -34,7 +34,7 @@ def main(): print("=" * 70) print(" Benchmark Suite") - print(f" Date: {datetime.now():%Y-%m-%d %H:%M:%S}") + print(f" Date: {datetime.now().astimezone():%Y-%m-%d %H:%M:%S}") print(f" Running: {', '.join(targets)}") print("=" * 70) diff --git a/benchmarks/stats.py b/benchmarks/stats.py index 5f1f912b..5520663d 100644 --- a/benchmarks/stats.py +++ b/benchmarks/stats.py @@ -54,7 +54,7 @@ def p99(self) -> float: def _percentile(self, p: float) -> float: if not self.latencies_ms: return 0.0 - idx = max(int(math.ceil(p * self.count)) - 1, 0) + idx = max(math.ceil(p * self.count) - 1, 0) return self.latencies_ms[idx] def summary(self) -> str: diff --git a/examples/cookbook/agentphone/create_index.py b/examples/cookbook/agentphone/create_index.py index 2bd90d00..cded32c2 100644 --- a/examples/cookbook/agentphone/create_index.py +++ b/examples/cookbook/agentphone/create_index.py @@ -16,7 +16,6 @@ from dotenv import load_dotenv from moss import DocumentInfo, MossClient - DEMO_DOCS = [ DocumentInfo( id="refunds", diff --git a/examples/cookbook/agentphone/server.py b/examples/cookbook/agentphone/server.py index 4079e6be..0f80fbfd 100644 --- a/examples/cookbook/agentphone/server.py +++ b/examples/cookbook/agentphone/server.py @@ -24,7 +24,6 @@ from fastapi import FastAPI, Header, HTTPException, Request from fastapi.responses import StreamingResponse from moss import MossClient, QueryOptions - from moss_agentphone import ( log_moss_search, ndjson, @@ -33,7 +32,6 @@ verify_webhook_signature, ) - logging.basicConfig(level=logging.INFO, format="%(levelname)-7s %(message)s") logger = logging.getLogger("moss_agentphone.server") diff --git a/examples/cookbook/agentphone/test_integration.py b/examples/cookbook/agentphone/test_integration.py index 2cd1ef7f..cbfc5b37 100644 --- a/examples/cookbook/agentphone/test_integration.py +++ b/examples/cookbook/agentphone/test_integration.py @@ -249,11 +249,10 @@ class WebhookRouteTests(unittest.TestCase): @classmethod def setUpClass(cls) -> None: - from fastapi.testclient import TestClient - # Import server only inside the test scope so env defaults above # are guaranteed to be set first. import server # noqa: WPS433 + from fastapi.testclient import TestClient cls.server = server cls.client = TestClient(server.app) diff --git a/examples/cookbook/crewai/example_usage.py b/examples/cookbook/crewai/example_usage.py index a17b1555..b715de65 100644 --- a/examples/cookbook/crewai/example_usage.py +++ b/examples/cookbook/crewai/example_usage.py @@ -5,7 +5,6 @@ from crewai import LLM, Agent, Crew, Task from dotenv import load_dotenv from moss import DocumentInfo, MossClient - from moss_crewai import MossSearchTool load_dotenv() @@ -41,11 +40,15 @@ } +def _load_json(path: str): + with open(path) as f: + return json.load(f) + + async def setup_indexes(): """Create travel indexes from Moss-formatted data.""" for index_name, config in INDEXES.items(): - with open(os.path.join(DATA_DIR, config["file"])) as f: - raw = json.load(f) + raw = await asyncio.to_thread(_load_json, os.path.join(DATA_DIR, config["file"])) docs = [ DocumentInfo(id=item["id"], text=item["text"], metadata=item.get("metadata", {})) diff --git a/examples/cookbook/crewai/moss_crewai.py b/examples/cookbook/crewai/moss_crewai.py index 831e4278..9f0e2c15 100644 --- a/examples/cookbook/crewai/moss_crewai.py +++ b/examples/cookbook/crewai/moss_crewai.py @@ -3,7 +3,13 @@ from typing import Any from crewai.tools import BaseTool -from moss import DocumentInfo, GetDocumentsOptions, MossClient, MutationOptions, QueryOptions +from moss import ( + DocumentInfo, + GetDocumentsOptions, + MossClient, + MutationOptions, + QueryOptions, +) from pydantic import BaseModel, Field, PrivateAttr @@ -203,7 +209,7 @@ async def _arun(self, index_name: str) -> str: class MossListIndexesInput(BaseModel): """Input schema for MossListIndexesTool.""" - pass # No input needed + # No input needed class MossListIndexesTool(MossBaseTool): diff --git a/examples/cookbook/crewai/test_live.py b/examples/cookbook/crewai/test_live.py index b647d793..e8c25f73 100644 --- a/examples/cookbook/crewai/test_live.py +++ b/examples/cookbook/crewai/test_live.py @@ -4,7 +4,6 @@ from dotenv import load_dotenv from moss import MossClient - from moss_crewai import ( MossAddDocsTool, MossCreateIndexTool, diff --git a/examples/cookbook/generalist-moss-voice-agent/create_index.py b/examples/cookbook/generalist-moss-voice-agent/create_index.py index 94f957ab..7df02ec9 100644 --- a/examples/cookbook/generalist-moss-voice-agent/create_index.py +++ b/examples/cookbook/generalist-moss-voice-agent/create_index.py @@ -15,7 +15,6 @@ import asyncio import os from pathlib import Path -from typing import List from dotenv import load_dotenv from moss import DocumentInfo, MossClient @@ -26,12 +25,12 @@ DEFAULT_MODEL_ID = "moss-minilm" -def load_docs_from_dir(docs_dir: Path) -> List[DocumentInfo]: +def load_docs_from_dir(docs_dir: Path) -> list[DocumentInfo]: """Read all supported text files from *docs_dir* recursively.""" if not docs_dir.exists(): raise FileNotFoundError(f"Docs directory not found: {docs_dir}") - documents: List[DocumentInfo] = [] + documents: list[DocumentInfo] = [] for path in sorted(docs_dir.rglob("*")): if path.suffix.lower() not in SUPPORTED_EXTENSIONS: continue @@ -70,7 +69,7 @@ async def create_index(index_name: str, docs_dir: Path) -> None: if not val ] if missing: - raise EnvironmentError( + raise OSError( "Missing required environment variables: " + ", ".join(missing) ) diff --git a/examples/cookbook/haystack/example_usage.py b/examples/cookbook/haystack/example_usage.py index a033897d..23101714 100644 --- a/examples/cookbook/haystack/example_usage.py +++ b/examples/cookbook/haystack/example_usage.py @@ -7,7 +7,6 @@ from haystack.components.generators import OpenAIGenerator from haystack.components.joiners import DocumentJoiner from haystack.utils import Secret - from moss_haystack import MossDocumentStore, MossRetriever load_dotenv() diff --git a/examples/cookbook/haystack/moss_haystack.py b/examples/cookbook/haystack/moss_haystack.py index fd68d1df..bd93a009 100644 --- a/examples/cookbook/haystack/moss_haystack.py +++ b/examples/cookbook/haystack/moss_haystack.py @@ -17,7 +17,7 @@ import asyncio import json import os -from typing import Any, Optional +from typing import Any from haystack import Document, component, default_from_dict, default_to_dict from haystack.document_stores.types import DocumentStore, DuplicatePolicy @@ -47,7 +47,7 @@ def _run_async(coro: Any) -> Any: _MOSS_TYPED_PREFIX = "__moss_typed__:" -def _serialize_metadata(meta: Optional[dict]) -> Optional[dict]: +def _serialize_metadata(meta: dict | None) -> dict | None: """Convert arbitrary-typed metadata to Moss string-only metadata. Moss only accepts string values in metadata. Non-string values are @@ -66,7 +66,7 @@ def _serialize_metadata(meta: Optional[dict]) -> Optional[dict]: return result -def _deserialize_metadata(meta: Optional[dict]) -> dict: +def _deserialize_metadata(meta: dict | None) -> dict: """Convert Moss string metadata back to original types. Values prefixed with ``__moss_typed__:`` are JSON-decoded to restore @@ -98,7 +98,7 @@ def _haystack_doc_to_moss(doc: Document) -> DocumentInfo: ) -def _moss_doc_to_haystack(doc: Any, score: Optional[float] = None) -> Document: +def _moss_doc_to_haystack(doc: Any, score: float | None = None) -> Document: """Convert Moss DocumentInfo/QueryResultDocumentInfo to Haystack Document.""" return Document( id=doc.id, @@ -115,8 +115,8 @@ class MossDocumentStore(DocumentStore): def __init__( self, - project_id: Optional[str] = None, - project_key: Optional[Secret] = None, + project_id: str | None = None, + project_key: Secret | None = None, index_name: str = "default", model_id: str = "moss-minilm", ): @@ -208,7 +208,7 @@ def delete_documents(self, document_ids: list[str]) -> None: _run_async(self.client.delete_docs(self.index_name, document_ids)) self._index_loaded = False - def filter_documents(self, filters: Optional[dict] = None) -> list[Document]: + def filter_documents(self, filters: dict | None = None) -> list[Document]: """Return documents from the index. When ``filters`` is None, returns every document in the index — the @@ -277,7 +277,7 @@ def from_dict(cls, data: dict[str, Any]) -> "MossRetriever": def run( self, query: str, - top_k: Optional[int] = None, + top_k: int | None = None, ) -> dict[str, list[Document]]: """Run semantic search against Moss index. diff --git a/examples/cookbook/haystack/test_live.py b/examples/cookbook/haystack/test_live.py index 44879ac0..d32bbd1f 100644 --- a/examples/cookbook/haystack/test_live.py +++ b/examples/cookbook/haystack/test_live.py @@ -6,7 +6,6 @@ from haystack import Document from haystack.document_stores.types import DuplicatePolicy from moss import MossClient - from moss_haystack import MossDocumentStore, MossRetriever load_dotenv() diff --git a/examples/cookbook/langchain/moss_langchain.py b/examples/cookbook/langchain/moss_langchain.py index ed69588d..2ee75b18 100644 --- a/examples/cookbook/langchain/moss_langchain.py +++ b/examples/cookbook/langchain/moss_langchain.py @@ -1,11 +1,16 @@ -from typing import Any, List, Optional -from pydantic import Field, PrivateAttr -from langchain_core.retrievers import BaseRetriever -from langchain_core.callbacks import CallbackManagerForRetrieverRun, AsyncCallbackManagerForRetrieverRun +import asyncio +from typing import Any + +from langchain_core.callbacks import ( + AsyncCallbackManagerForRetrieverRun, + CallbackManagerForRetrieverRun, +) from langchain_core.documents import Document +from langchain_core.retrievers import BaseRetriever from langchain_core.tools import Tool from moss import MossClient, QueryOptions -import asyncio +from pydantic import Field, PrivateAttr + class MossRetriever(BaseRetriever): """Moss semantic search retriever.""" @@ -30,7 +35,7 @@ async def _ensure_loaded(self) -> None: def _get_relevant_documents( self, query: str, *, run_manager: CallbackManagerForRetrieverRun - ) -> List[Document]: + ) -> list[Document]: """Synchronous retrieval. Note: This will fail if called from a running event loop (like a Jupyter notebook). In such cases, use `ainvoke` instead. @@ -43,11 +48,11 @@ def _get_relevant_documents( "MossRetriever.invoke() cannot be called from a running event loop (e.g., in a Jupyter notebook). " "Please use 'await MossRetriever.ainvoke()' instead." ) from e - raise e + raise async def _aget_relevant_documents( - self, query: str, *, run_manager: Optional[AsyncCallbackManagerForRetrieverRun] = None - ) -> List[Document]: + self, query: str, *, run_manager: AsyncCallbackManagerForRetrieverRun | None = None + ) -> list[Document]: """Asynchronous retrieval from Moss.""" await self._ensure_loaded() diff --git a/examples/cookbook/langchain/test_integration.py b/examples/cookbook/langchain/test_integration.py index 806f5ff8..a0abef5f 100644 --- a/examples/cookbook/langchain/test_integration.py +++ b/examples/cookbook/langchain/test_integration.py @@ -1,7 +1,8 @@ import unittest -from unittest.mock import AsyncMock, patch, MagicMock -from moss_langchain import MossRetriever, get_moss_tool +from unittest.mock import AsyncMock, MagicMock, patch + from langchain_core.documents import Document +from moss_langchain import MossRetriever, get_moss_tool class TestMossRetriever(unittest.IsolatedAsyncioTestCase): diff --git a/examples/cookbook/langflow/moss_langflow.py b/examples/cookbook/langflow/moss_langflow.py index 2db811ee..d85469cc 100644 --- a/examples/cookbook/langflow/moss_langflow.py +++ b/examples/cookbook/langflow/moss_langflow.py @@ -21,7 +21,7 @@ import asyncio import json import os -from typing import Any, Optional +from typing import Any from langflow.custom import Component from langflow.io import ( @@ -35,7 +35,6 @@ from langflow.schema.message import Message from moss import MossClient, QueryOptions - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- @@ -59,7 +58,7 @@ def _run_async(coro: Any) -> Any: raise -def _parse_filter(raw: str) -> Optional[dict]: +def _parse_filter(raw: str) -> dict | None: """Parse an optional JSON filter string into a dict. Returns ``None`` when *raw* is empty or whitespace-only. diff --git a/examples/cookbook/langflow/test_integration.py b/examples/cookbook/langflow/test_integration.py index dd24d0e7..42933568 100644 --- a/examples/cookbook/langflow/test_integration.py +++ b/examples/cookbook/langflow/test_integration.py @@ -10,7 +10,6 @@ import unittest from unittest.mock import AsyncMock, MagicMock, patch - # --------------------------------------------------------------------------- # Guard: skip the entire module if langflow is not installed. # --------------------------------------------------------------------------- diff --git a/examples/cookbook/langgraph/example_usage.py b/examples/cookbook/langgraph/example_usage.py index 12d1975d..a8d10e75 100644 --- a/examples/cookbook/langgraph/example_usage.py +++ b/examples/cookbook/langgraph/example_usage.py @@ -9,7 +9,6 @@ from dotenv import load_dotenv from langchain_groq import ChatGroq from moss import MossClient - from moss_langgraph import ( LangGraphMossState, ask_question, diff --git a/examples/cookbook/langgraph/moss_langgraph.py b/examples/cookbook/langgraph/moss_langgraph.py index 58ed8179..da14c716 100644 --- a/examples/cookbook/langgraph/moss_langgraph.py +++ b/examples/cookbook/langgraph/moss_langgraph.py @@ -98,10 +98,12 @@ async def generate(state: LangGraphMossState) -> dict[str, str]: ("system", SYSTEM_PROMPT), ( "human", - "Question:\n" - f"{state['query']}\n\n" - "Retrieved Moss context:\n" - f"{state.get('retrieval_context', 'No retrieval context available.')}", + ( + "Question:\n" + f"{state['query']}\n\n" + "Retrieved Moss context:\n" + f"{state.get('retrieval_context', 'No retrieval context available.')}" + ), ), ] ) diff --git a/examples/cookbook/langgraph/test_integration.py b/examples/cookbook/langgraph/test_integration.py index 905fea42..aa4aa454 100644 --- a/examples/cookbook/langgraph/test_integration.py +++ b/examples/cookbook/langgraph/test_integration.py @@ -5,15 +5,15 @@ sys.path.insert(0, os.path.dirname(__file__)) -from moss_langgraph import ( - ask_question, - build_moss_graph, -) from example_usage import ( _load_index_before_graph_runs, _parse_filter_eq, run_langgraph_agent, ) +from moss_langgraph import ( + ask_question, + build_moss_graph, +) class TestLangGraphHelpers(unittest.TestCase): diff --git a/examples/cookbook/openai-agents/example.py b/examples/cookbook/openai-agents/example.py index d228e05e..55650d78 100644 --- a/examples/cookbook/openai-agents/example.py +++ b/examples/cookbook/openai-agents/example.py @@ -1,7 +1,7 @@ import asyncio import json import os -from typing import Any, Dict, Optional +from typing import Any from agents import Agent, Runner, function_tool from dotenv import load_dotenv @@ -17,7 +17,7 @@ ) -def _require_env_vars(names: tuple[str, ...] = REQUIRED_ENV_VARS) -> Dict[str, str]: +def _require_env_vars(names: tuple[str, ...] = REQUIRED_ENV_VARS) -> dict[str, str]: """Return required environment variables or raise a clear setup error.""" values = {name: os.getenv(name) for name in names} missing = [name for name, value in values.items() if not value] @@ -97,7 +97,7 @@ async def search_moss( index_name: str, query: str, top_k: int = DEFAULT_TOP_K, - filter: Optional[Dict[str, Any]] = None, + filter: dict[str, Any] | None = None, ) -> str: """Query Moss and format results for the OpenAI agent.""" top_k = _validate_top_k(top_k) @@ -116,7 +116,7 @@ def create_moss_search_tool(client: MossClient, index_name: str) -> Any: async def moss_search( query: str, top_k: int = DEFAULT_TOP_K, - filter: Optional[Dict[str, Any]] = None, + filter: dict[str, Any] | None = None, ) -> str: """Search the knowledge base for answers to the user's question. diff --git a/examples/cookbook/openai-agents/test_example.py b/examples/cookbook/openai-agents/test_example.py index 684375d2..a297134b 100644 --- a/examples/cookbook/openai-agents/test_example.py +++ b/examples/cookbook/openai-agents/test_example.py @@ -2,10 +2,10 @@ import importlib import os -from pathlib import Path import sys import types import unittest +from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch @@ -187,9 +187,11 @@ async def test_structured_results_include_grounding_fields(self): class TestEnvironmentValidation(unittest.TestCase): def test_missing_env_vars_are_reported_together(self): - with patch.dict(os.environ, {}, clear=True): - with self.assertRaisesRegex(RuntimeError, "MOSS_PROJECT_ID"): - example._require_env_vars() + with ( + patch.dict(os.environ, {}, clear=True), + self.assertRaisesRegex(RuntimeError, "MOSS_PROJECT_ID"), + ): + example._require_env_vars() try: with patch.dict(os.environ, {}, clear=True): diff --git a/examples/cookbook/pydantic-ai/example.py b/examples/cookbook/pydantic-ai/example.py index d118e615..19ef7209 100644 --- a/examples/cookbook/pydantic-ai/example.py +++ b/examples/cookbook/pydantic-ai/example.py @@ -5,9 +5,8 @@ from dotenv import load_dotenv from moss import DocumentInfo, MossClient -from pydantic_ai import Agent - from moss_pydantic_ai import MossSearchTool +from pydantic_ai import Agent def _require_env(name: str) -> str: diff --git a/examples/cookbook/pydantic-ai/moss_pydantic_ai.py b/examples/cookbook/pydantic-ai/moss_pydantic_ai.py index 70b48b4e..bb7b4001 100644 --- a/examples/cookbook/pydantic-ai/moss_pydantic_ai.py +++ b/examples/cookbook/pydantic-ai/moss_pydantic_ai.py @@ -1,6 +1,8 @@ from __future__ import annotations + from collections.abc import Sequence from typing import Any + from moss import MossClient, QueryOptions from pydantic_ai import Tool diff --git a/examples/cookbook/pydantic-ai/test_integration.py b/examples/cookbook/pydantic-ai/test_integration.py index 1fec9643..1833e1bb 100644 --- a/examples/cookbook/pydantic-ai/test_integration.py +++ b/examples/cookbook/pydantic-ai/test_integration.py @@ -10,7 +10,6 @@ from moss_pydantic_ai import MossSearchTool - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- diff --git a/examples/cookbook/sim/server.py b/examples/cookbook/sim/server.py index f88dc01e..d2744972 100644 --- a/examples/cookbook/sim/server.py +++ b/examples/cookbook/sim/server.py @@ -25,7 +25,6 @@ from dotenv import load_dotenv from fastapi import FastAPI, HTTPException from pydantic import BaseModel - from sim_moss import MossSimSearch load_dotenv() diff --git a/examples/cookbook/smolagents/moss_smol_agent_demo.py b/examples/cookbook/smolagents/moss_smol_agent_demo.py index 96dcd24c..51091cef 100644 --- a/examples/cookbook/smolagents/moss_smol_agent_demo.py +++ b/examples/cookbook/smolagents/moss_smol_agent_demo.py @@ -6,9 +6,8 @@ from dotenv import load_dotenv from moss import DocumentInfo, MossClient -from smolagents import CodeAgent - from moss_smolagents import MossSearchTool +from smolagents import CodeAgent def _require_env(name: str) -> str: diff --git a/examples/cookbook/smolagents/moss_smolagents.py b/examples/cookbook/smolagents/moss_smolagents.py index 80226fe9..2730af2b 100644 --- a/examples/cookbook/smolagents/moss_smolagents.py +++ b/examples/cookbook/smolagents/moss_smolagents.py @@ -1,7 +1,8 @@ from __future__ import annotations import asyncio -from typing import Any, Optional, Sequence +from collections.abc import Sequence +from typing import Any from moss import MossClient, QueryOptions from smolagents import Tool @@ -29,9 +30,9 @@ class MossSearchTool(Tool): def __init__( self, *, - client: Optional[MossClient] = None, - project_id: Optional[str] = None, - project_key: Optional[str] = None, + client: MossClient | None = None, + project_id: str | None = None, + project_key: str | None = None, index_name: str, tool_name: str = "moss_search", top_k: int = 5, diff --git a/examples/cookbook/unstructured/ingest.py b/examples/cookbook/unstructured/ingest.py index 17177bbb..b0a90381 100644 --- a/examples/cookbook/unstructured/ingest.py +++ b/examples/cookbook/unstructured/ingest.py @@ -11,7 +11,6 @@ from unstructured.chunking.title import chunk_by_title from unstructured.partition.auto import partition - DEFAULT_MAX_CHARACTERS = 1_500 DEFAULT_NEW_AFTER_N_CHARS = 1_200 DEFAULT_COMBINE_UNDER_N_CHARS = 300 diff --git a/examples/moss-pikachu/MossPikachu/Resources/moss_worker.py b/examples/moss-pikachu/MossPikachu/Resources/moss_worker.py old mode 100644 new mode 100755 index f3196ab3..f85aa0e1 --- a/examples/moss-pikachu/MossPikachu/Resources/moss_worker.py +++ b/examples/moss-pikachu/MossPikachu/Resources/moss_worker.py @@ -8,8 +8,8 @@ import os import re import resource -import signal import shutil +import signal import sys import time from datetime import datetime, timezone diff --git a/examples/python-classification/classify_sample.py b/examples/python-classification/classify_sample.py index 464637d6..e0cc57ec 100644 --- a/examples/python-classification/classify_sample.py +++ b/examples/python-classification/classify_sample.py @@ -16,7 +16,6 @@ import time from dotenv import load_dotenv - from moss_classify_rest import MossClassifyClient load_dotenv() diff --git a/examples/python-moss-agent/agent.py b/examples/python-moss-agent/agent.py index 87f62ec0..6465c6d2 100644 --- a/examples/python-moss-agent/agent.py +++ b/examples/python-moss-agent/agent.py @@ -54,8 +54,8 @@ from dotenv import load_dotenv from livekit.agents import ( Agent, - AgentSession, AgentServer, + AgentSession, JobContext, JobProcess, RunContext, @@ -63,7 +63,6 @@ function_tool, ) from livekit.plugins import cartesia, deepgram, openai, silero - from moss_agent import MossAgent, MossCall, QueryOptions load_dotenv() diff --git a/examples/python/comprehensive_sample.py b/examples/python/comprehensive_sample.py index 46735123..23a48f85 100644 --- a/examples/python/comprehensive_sample.py +++ b/examples/python/comprehensive_sample.py @@ -14,9 +14,9 @@ import asyncio import os -from typing import List -from dotenv import load_dotenv from datetime import datetime + +from dotenv import load_dotenv from moss import ( DocumentInfo, GetDocumentsOptions, @@ -62,7 +62,7 @@ async def comprehensive_moss_example(): client = MossClient(project_id, project_key) # Create comprehensive document collection with rich metadata - documents: List[DocumentInfo] = [ + documents: list[DocumentInfo] = [ DocumentInfo( id="tech-ai-001", text="Artificial Intelligence (AI) is transforming industries by enabling machines to perform tasks that typically require human intelligence. From healthcare diagnostics to autonomous vehicles, AI applications are revolutionizing how we work and live.", @@ -178,7 +178,7 @@ async def comprehensive_moss_example(): ] # Create dynamic index name with timestamp - timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") + timestamp = datetime.now().astimezone().strftime("%Y%m%d-%H%M%S") index_name = f"comprehensive-demo-{timestamp}" try: @@ -202,7 +202,7 @@ async def comprehensive_moss_example(): print(f" - {idx.name}: {idx.doc_count} docs, status: {idx.status}") print("\nStep 4: Adding additional documents with upsert...") - additional_docs: List[DocumentInfo] = [ + additional_docs: list[DocumentInfo] = [ DocumentInfo( id="security-cyber-008", text="Cybersecurity protects digital systems, networks, and data from cyber threats. It involves implementing security measures, monitoring for vulnerabilities, and responding to incidents to maintain data integrity and privacy.", diff --git a/examples/python/custom_embedding_sample.py b/examples/python/custom_embedding_sample.py index 35ebf651..9219279d 100644 --- a/examples/python/custom_embedding_sample.py +++ b/examples/python/custom_embedding_sample.py @@ -4,17 +4,15 @@ import asyncio import os import time -from typing import List from dotenv import load_dotenv -from openai import OpenAI - from moss import ( DocumentInfo, MossClient, MutationOptions, QueryOptions, ) +from openai import OpenAI # ---------- Configuration ---------- load_dotenv() @@ -104,7 +102,7 @@ # ---------- Helper Functions ---------- -def generate_embedding(text: str) -> List[float]: +def generate_embedding(text: str) -> list[float]: """Generate an OpenAI embedding for the provided text.""" response = openai_client.embeddings.create( diff --git a/examples/python/load_and_query_sample.py b/examples/python/load_and_query_sample.py index 985efd86..5404303a 100644 --- a/examples/python/load_and_query_sample.py +++ b/examples/python/load_and_query_sample.py @@ -12,6 +12,7 @@ import asyncio import os + from dotenv import load_dotenv from moss import MossClient, QueryOptions diff --git a/examples/python/metadata_filtering.py b/examples/python/metadata_filtering.py index 19bab875..73675196 100644 --- a/examples/python/metadata_filtering.py +++ b/examples/python/metadata_filtering.py @@ -7,7 +7,6 @@ import asyncio import os from datetime import datetime -from typing import List from dotenv import load_dotenv from moss import DocumentInfo, MossClient, QueryOptions @@ -30,7 +29,7 @@ async def metadata_filtering_sample() -> None: client = MossClient(project_id, project_key) - documents: List[DocumentInfo] = [ + documents: list[DocumentInfo] = [ DocumentInfo( id="doc1", text="Running shoes with breathable mesh for daily training.", @@ -66,7 +65,7 @@ async def metadata_filtering_sample() -> None: ), ] - index_name = f"metadata-filter-sample-{datetime.now().strftime('%Y%m%d-%H%M%S')}" + index_name = f"metadata-filter-sample-{datetime.now().astimezone().strftime('%Y%m%d-%H%M%S')}" try: print("\n1. Creating index...") diff --git a/examples/python/multi_index_search.py b/examples/python/multi_index_search.py index 18c16932..2a5ae317 100644 --- a/examples/python/multi_index_search.py +++ b/examples/python/multi_index_search.py @@ -17,10 +17,8 @@ import asyncio import os from datetime import datetime -from typing import List from dotenv import load_dotenv - from moss import ( DocumentInfo, LoadIndexesResult, @@ -32,7 +30,7 @@ load_dotenv() -PRODUCT_DOCS: List[DocumentInfo] = [ +PRODUCT_DOCS: list[DocumentInfo] = [ DocumentInfo( id="p1", text="Sony WH-1000XM5 wireless noise-cancelling headphones with 30-hour battery life.", @@ -50,7 +48,7 @@ ), ] -REVIEW_DOCS: List[DocumentInfo] = [ +REVIEW_DOCS: list[DocumentInfo] = [ DocumentInfo( id="r1", text="Battery on these wireless headphones easily lasts a 30-hour transatlantic week.", @@ -68,7 +66,7 @@ ), ] -FAQ_DOCS: List[DocumentInfo] = [ +FAQ_DOCS: list[DocumentInfo] = [ DocumentInfo( id="f1", text="How long does the battery last on wireless noise-cancelling headphones?", @@ -101,7 +99,7 @@ async def multi_index_search_sample() -> None: client = MossClient(project_id, project_key) - timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") + timestamp = datetime.now().astimezone().strftime("%Y%m%d-%H%M%S") products_index = f"sample-multi-products-{timestamp}" reviews_index = f"sample-multi-reviews-{timestamp}" faqs_index = f"sample-multi-faqs-{timestamp}" diff --git a/examples/python/session_sample.py b/examples/python/session_sample.py index d02e291b..83de9441 100644 --- a/examples/python/session_sample.py +++ b/examples/python/session_sample.py @@ -13,8 +13,9 @@ import asyncio import os + from dotenv import load_dotenv -from moss import MossClient, DocumentInfo, QueryOptions +from moss import DocumentInfo, MossClient, QueryOptions # Load environment variables load_dotenv() diff --git a/examples/voice-agents/airline-pnr/agent.py b/examples/voice-agents/airline-pnr/agent.py index 0be44673..dbad0209 100644 --- a/examples/voice-agents/airline-pnr/agent.py +++ b/examples/voice-agents/airline-pnr/agent.py @@ -48,7 +48,6 @@ import time from dataclasses import dataclass, field from pathlib import Path -from typing import Optional from dotenv import load_dotenv from livekit.agents import ( @@ -63,7 +62,6 @@ function_tool, ) from livekit.plugins import cartesia, deepgram, openai, silero - from moss import MossClient, QueryOptions load_dotenv() @@ -140,8 +138,8 @@ class CallSessionData: started_at: float = field(default_factory=time.time) - active_pnr: Optional[str] = None - active_index: Optional[str] = None + active_pnr: str | None = None + active_index: str | None = None bookings_loaded: list[str] = field(default_factory=list) # all PNRs touched in this call caller_verified: bool = False @@ -152,7 +150,7 @@ class CallSessionData: change_requests: list[ChangeRequest] = field(default_factory=list) notes: list[str] = field(default_factory=list) - moss_client: Optional[MossClient] = None + moss_client: MossClient | None = None # --------------------------------------------------------------------------- diff --git a/examples/voice-agents/airline-pnr/evals/test_call_summary.py b/examples/voice-agents/airline-pnr/evals/test_call_summary.py index 4a3c0c71..88d38848 100644 --- a/examples/voice-agents/airline-pnr/evals/test_call_summary.py +++ b/examples/voice-agents/airline-pnr/evals/test_call_summary.py @@ -19,7 +19,7 @@ # Make the agent module importable without installing the package. sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) -from agent import ( # noqa: E402 +from agent import ( CallSessionData, ChangeRequest, _build_summary, diff --git a/examples/voice-agents/candidate-screening/agent.py b/examples/voice-agents/candidate-screening/agent.py index 0c887967..1d632705 100644 --- a/examples/voice-agents/candidate-screening/agent.py +++ b/examples/voice-agents/candidate-screening/agent.py @@ -33,7 +33,6 @@ import time from dataclasses import dataclass, field from pathlib import Path -from typing import Optional from dotenv import load_dotenv from livekit.agents import ( @@ -46,7 +45,6 @@ function_tool, ) from livekit.plugins import cartesia, deepgram, openai, silero - from moss import MossClient, QueryOptions load_dotenv() @@ -110,12 +108,12 @@ class ScreeningSessionData: candidate_id: str role_id: str - consent_to_record: Optional[bool] = None + consent_to_record: bool | None = None started_at: float = field(default_factory=time.time) rubric: dict[str, RubricEntry] = field(default_factory=dict) candidate_questions: list[CandidateQuestion] = field(default_factory=list) notes: list[str] = field(default_factory=list) - moss_client: Optional[MossClient] = None + moss_client: MossClient | None = None # --------------------------------------------------------------------------- diff --git a/examples/voice-agents/candidate-screening/evals/test_scorecards.py b/examples/voice-agents/candidate-screening/evals/test_scorecards.py index 5f00bd4f..a7610711 100644 --- a/examples/voice-agents/candidate-screening/evals/test_scorecards.py +++ b/examples/voice-agents/candidate-screening/evals/test_scorecards.py @@ -20,7 +20,7 @@ # Make the agent module importable without installing the package. sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) -from agent import ( # noqa: E402 +from agent import ( RubricEntry, ScreeningSessionData, _build_scorecard, diff --git a/examples/voice-agents/insurance-adjuster/agent.py b/examples/voice-agents/insurance-adjuster/agent.py index c9b796d4..86a521f7 100644 --- a/examples/voice-agents/insurance-adjuster/agent.py +++ b/examples/voice-agents/insurance-adjuster/agent.py @@ -38,7 +38,6 @@ function_tool, ) from livekit.plugins import cartesia, deepgram, openai - from moss import DocumentInfo, MossClient, QueryOptions, SessionIndex load_dotenv() diff --git a/examples/voice-agents/insurance-adjuster/create_indexes.py b/examples/voice-agents/insurance-adjuster/create_indexes.py index 42726540..1a618a2f 100644 --- a/examples/voice-agents/insurance-adjuster/create_indexes.py +++ b/examples/voice-agents/insurance-adjuster/create_indexes.py @@ -80,7 +80,7 @@ def _discover_policies() -> dict[str, Path]: text = summary["text"] import re - m = re.search(r"Policy number\s+([\w\-]+)", text, re.I) + m = re.search(r"Policy number\s+([\w\-]+)", text, re.IGNORECASE) if m: policy_num = m.group(1).strip().upper() out[policy_num] = path diff --git a/examples/voice-agents/mortgage-lending/agent.py b/examples/voice-agents/mortgage-lending/agent.py index 2049c807..f46d9a03 100644 --- a/examples/voice-agents/mortgage-lending/agent.py +++ b/examples/voice-agents/mortgage-lending/agent.py @@ -24,7 +24,6 @@ import logging import os from dataclasses import dataclass, field -from typing import Optional from dotenv import load_dotenv from livekit.agents import ( @@ -37,7 +36,6 @@ function_tool, ) from livekit.plugins import cartesia, deepgram, openai, silero - from moss import MossClient, QueryOptions load_dotenv() @@ -85,13 +83,13 @@ class MortgageSessionData: (which would silently fall back to the slow cloud query API). """ - loan_number: Optional[str] = None - customer_name: Optional[str] = None - last_four_ssn: Optional[str] = None - payment_amount: Optional[float] = None - payment_method: Optional[str] = None + loan_number: str | None = None + customer_name: str | None = None + last_four_ssn: str | None = None + payment_amount: float | None = None + payment_method: str | None = None questions_answered: list[str] = field(default_factory=list) - moss_client: Optional[MossClient] = None + moss_client: MossClient | None = None # --------------------------------------------------------------------------- diff --git a/moss-live-labs/community-demos/voice-agents/bharat-benefits/bot.py b/moss-live-labs/community-demos/voice-agents/bharat-benefits/bot.py index a240c553..a978d763 100644 --- a/moss-live-labs/community-demos/voice-agents/bharat-benefits/bot.py +++ b/moss-live-labs/community-demos/voice-agents/bharat-benefits/bot.py @@ -40,7 +40,6 @@ import re import sys import tempfile -import time from pathlib import Path import httpx @@ -145,10 +144,10 @@ def record_fixed(seconds: int = 5) -> Path: print(f"ERROR: microphone unavailable: {e}", file=sys.stderr) sys.exit(1) - tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) - sf.write(tmp.name, audio, SAMPLE_RATE, subtype="PCM_16") - tmp.close() - return Path(tmp.name) + fd, wav_path = tempfile.mkstemp(suffix=".wav") + os.close(fd) + sf.write(wav_path, audio, SAMPLE_RATE, subtype="PCM_16") + return Path(wav_path) # -- Recording: VAD (voice activity detection) -------------------------------- @@ -225,10 +224,10 @@ def record_vad() -> Path: audio_bytes = b"".join(frames_recorded) audio_array = np.frombuffer(audio_bytes, dtype=np.int16) - tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) - sf.write(tmp.name, audio_array, SAMPLE_RATE, subtype="PCM_16") - tmp.close() - return Path(tmp.name) + fd, wav_path = tempfile.mkstemp(suffix=".wav") + os.close(fd) + sf.write(wav_path, audio_array, SAMPLE_RATE, subtype="PCM_16") + return Path(wav_path) # -- Speech to text (Sarvam STT) ---------------------------------------------- @@ -350,9 +349,8 @@ def speak(answer: str) -> None: wav_bytes = base64.b64decode(audios[0]) - tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) - tmp.write(wav_bytes) - tmp.close() + with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp: + tmp.write(wav_bytes) try: import simpleaudio as sa @@ -458,11 +456,11 @@ async def live_loop( except httpx.HTTPStatusError as e: print(f"\nAPI error {e.response.status_code}: {e.response.text[:200]}") print("Retrying next turn ...") - time.sleep(1) + await asyncio.sleep(1) except Exception as e: print(f"\nERROR: {e}") print("Retrying next turn ...") - time.sleep(1) + await asyncio.sleep(1) # -- Entry -------------------------------------------------------------------- diff --git a/moss-live-labs/examples/advanced-voice-agent/agent/agent.py b/moss-live-labs/examples/advanced-voice-agent/agent/agent.py index f27e3205..a6d0d110 100644 --- a/moss-live-labs/examples/advanced-voice-agent/agent/agent.py +++ b/moss-live-labs/examples/advanced-voice-agent/agent/agent.py @@ -1,17 +1,17 @@ # Imports and setup import logging import os + from dotenv import load_dotenv -from livekit.plugins import google, deepgram, silero, cartesia from livekit.agents import ( + Agent, + AgentSession, JobContext, WorkerOptions, cli, - Agent, - AgentSession, function_tool, ) - +from livekit.plugins import cartesia, deepgram, google, silero from moss import MossClient, QueryOptions load_dotenv() diff --git a/moss-live-labs/examples/advanced-voice-agent/moss-utils/create_index.py b/moss-live-labs/examples/advanced-voice-agent/moss-utils/create_index.py index d96ae690..f57ed7ee 100644 --- a/moss-live-labs/examples/advanced-voice-agent/moss-utils/create_index.py +++ b/moss-live-labs/examples/advanced-voice-agent/moss-utils/create_index.py @@ -2,6 +2,7 @@ import asyncio import os + from dotenv import load_dotenv load_dotenv() diff --git a/moss-live-labs/examples/image-search/setup-py/create_index.py b/moss-live-labs/examples/image-search/setup-py/create_index.py index bbaa4f5d..87168600 100644 --- a/moss-live-labs/examples/image-search/setup-py/create_index.py +++ b/moss-live-labs/examples/image-search/setup-py/create_index.py @@ -10,7 +10,6 @@ import json import os from pathlib import Path -from typing import List from dotenv import load_dotenv from moss import DocumentInfo, MossClient @@ -32,7 +31,7 @@ def _get_index_name(base_name: str, tier: str) -> str: return f"{base_name}-{tier}" -def _load_documents(tier: str) -> List[DocumentInfo]: +def _load_documents(tier: str) -> list[DocumentInfo]: """Load documents from the tiered COCO JSON file.""" data_path = _get_data_file_path(tier) if not data_path.exists(): @@ -46,7 +45,7 @@ def _load_documents(tier: str) -> List[DocumentInfo]: if not isinstance(data, list): raise ValueError("Data file must contain a JSON array.") - documents: List[DocumentInfo] = [] + documents: list[DocumentInfo] = [] for entry in data: if not isinstance(entry, dict): continue @@ -84,7 +83,7 @@ async def create_index_for_tier(tier: str) -> None: if not value ] if missing: - raise EnvironmentError( + raise OSError( "Missing required environment variables: " + ", ".join(missing) ) diff --git a/moss-live-labs/examples/semantic-cache/semantic_cache.py b/moss-live-labs/examples/semantic-cache/semantic_cache.py index e4179d35..5e16a3c6 100644 --- a/moss-live-labs/examples/semantic-cache/semantic_cache.py +++ b/moss-live-labs/examples/semantic-cache/semantic_cache.py @@ -17,10 +17,9 @@ import uuid from dotenv import load_dotenv +from moss import DocumentInfo, MossClient, QueryOptions from openai import AsyncOpenAI -from moss import MossClient, DocumentInfo, QueryOptions - # semantic similarity above which a cached answer is "close enough" to reuse. # too low -> you answer questions people didn't quite ask; too high -> you miss. THRESHOLD = 0.92 diff --git a/moss-live-labs/examples/voice-agent/agent.py b/moss-live-labs/examples/voice-agent/agent.py index d85b6606..9d0b389e 100644 --- a/moss-live-labs/examples/voice-agent/agent.py +++ b/moss-live-labs/examples/voice-agent/agent.py @@ -1,17 +1,17 @@ import logging import os + from dotenv import load_dotenv -from livekit.plugins import openai, deepgram, silero, cartesia from livekit.agents import ( + Agent, + AgentSession, + ChatContext, + ChatMessage, JobContext, WorkerOptions, cli, - ChatContext, - ChatMessage, - Agent, - AgentSession, ) - +from livekit.plugins import cartesia, deepgram, openai, silero # Moss Import from moss import MossClient, QueryOptions diff --git a/moss-live-labs/python/advance_query.py b/moss-live-labs/python/advance_query.py index 73e25b52..315411d5 100644 --- a/moss-live-labs/python/advance_query.py +++ b/moss-live-labs/python/advance_query.py @@ -1,7 +1,9 @@ -import os import asyncio -from moss import MossClient, QueryOptions +import os + from dotenv import load_dotenv +from moss import MossClient, QueryOptions + load_dotenv() diff --git a/moss-live-labs/python/simple_quickstart.py b/moss-live-labs/python/simple_quickstart.py index b2caeda0..7f749f41 100644 --- a/moss-live-labs/python/simple_quickstart.py +++ b/moss-live-labs/python/simple_quickstart.py @@ -1,7 +1,9 @@ -import os import asyncio -from moss import MossClient +import os + from dotenv import load_dotenv +from moss import MossClient + load_dotenv() diff --git a/moss-workshop/starter/build_index.py b/moss-workshop/starter/build_index.py index 8e266734..f31db3ca 100644 --- a/moss-workshop/starter/build_index.py +++ b/moss-workshop/starter/build_index.py @@ -8,7 +8,7 @@ from pathlib import Path from dotenv import load_dotenv -from moss import MossClient, DocumentInfo +from moss import DocumentInfo, MossClient load_dotenv() INDEX = os.getenv("MOSS_INDEX_NAME", "hackathon") diff --git a/moss-workshop/starter/voice_agent.py b/moss-workshop/starter/voice_agent.py index d1aabb46..0c4f5eae 100644 --- a/moss-workshop/starter/voice_agent.py +++ b/moss-workshop/starter/voice_agent.py @@ -11,12 +11,19 @@ import os from dotenv import load_dotenv -from livekit.plugins import openai, deepgram, silero from livekit.agents import ( - JobContext, WorkerOptions, cli, Agent, AgentSession, - ChatContext, ChatMessage, RunContext, function_tool, + Agent, + AgentSession, + ChatContext, + ChatMessage, + JobContext, + RunContext, + WorkerOptions, + cli, + function_tool, ) -from moss import MossClient, DocumentInfo, QueryOptions +from livekit.plugins import deepgram, openai, silero +from moss import DocumentInfo, MossClient, QueryOptions load_dotenv() INDEX = os.getenv("MOSS_INDEX_NAME", "hackathon") diff --git a/packages/moss-cli/pyproject.toml b/packages/moss-cli/pyproject.toml index 786ad57b..27ecc2a0 100644 --- a/packages/moss-cli/pyproject.toml +++ b/packages/moss-cli/pyproject.toml @@ -26,7 +26,8 @@ classifiers = [ ] dependencies = [ "moss>=1.1.1", - "typer>=0.9.0", + # 0.12.4 is the first release that accepts `X | None` parameter annotations. + "typer>=0.12.4", "rich>=13.0.0", ] diff --git a/packages/moss-cli/src/moss_cli/commands/doc.py b/packages/moss-cli/src/moss_cli/commands/doc.py index b71059a3..48976d7b 100644 --- a/packages/moss-cli/src/moss_cli/commands/doc.py +++ b/packages/moss-cli/src/moss_cli/commands/doc.py @@ -3,13 +3,11 @@ from __future__ import annotations import asyncio -from typing import Optional import typer +from moss import GetDocumentsOptions, MossClient, MutationOptions from rich.console import Console -from moss import MossClient, GetDocumentsOptions, MutationOptions - from .. import output from ..completion import complete_index_name from ..config import resolve_credentials @@ -32,13 +30,13 @@ def add( ctx: typer.Context, index_name: str = typer.Argument(..., help="Index name", autocompletion=complete_index_name), file: str = typer.Option(..., "--file", "-f", help="Path to JSON/CSV document file, or '-' for stdin"), - profile: Optional[str] = typer.Option( + profile: str | None = typer.Option( None, "--profile", help="Credential profile name" ), upsert: bool = typer.Option(False, "--upsert", "-u", help="Update existing documents"), wait: bool = typer.Option(False, "--wait", "-w", help="Wait for job to complete"), poll_interval: float = typer.Option(2.0, "--poll-interval", help="Seconds between status checks"), - timeout: Optional[float] = typer.Option(None, "--timeout", help="Max seconds to wait (requires --wait)"), + timeout: float | None = typer.Option(None, "--timeout", help="Max seconds to wait (requires --wait)"), ) -> None: """Add documents to an index.""" json_mode = ctx.obj.get("json_output", False) @@ -67,12 +65,12 @@ def delete( ctx: typer.Context, index_name: str = typer.Argument(..., help="Index name", autocompletion=complete_index_name), ids: str = typer.Option(..., "--ids", "-i", help="Comma-separated document IDs"), - profile: Optional[str] = typer.Option( + profile: str | None = typer.Option( None, "--profile", help="Credential profile name" ), wait: bool = typer.Option(False, "--wait", "-w", help="Wait for job to complete"), poll_interval: float = typer.Option(2.0, "--poll-interval", help="Seconds between status checks"), - timeout: Optional[float] = typer.Option(None, "--timeout", help="Max seconds to wait (requires --wait)"), + timeout: float | None = typer.Option(None, "--timeout", help="Max seconds to wait (requires --wait)"), ) -> None: """Delete documents from an index by ID.""" json_mode = ctx.obj.get("json_output", False) @@ -99,8 +97,8 @@ def delete( def get( ctx: typer.Context, index_name: str = typer.Argument(..., help="Index name", autocompletion=complete_index_name), - ids: Optional[str] = typer.Option(None, "--ids", "-i", help="Comma-separated document IDs (omit for all)"), - profile: Optional[str] = typer.Option( + ids: str | None = typer.Option(None, "--ids", "-i", help="Comma-separated document IDs (omit for all)"), + profile: str | None = typer.Option( None, "--profile", help="Credential profile name" ), ) -> None: diff --git a/packages/moss-cli/src/moss_cli/commands/index.py b/packages/moss-cli/src/moss_cli/commands/index.py index 3f255187..b4d0ac2e 100644 --- a/packages/moss-cli/src/moss_cli/commands/index.py +++ b/packages/moss-cli/src/moss_cli/commands/index.py @@ -3,12 +3,10 @@ from __future__ import annotations import asyncio -from typing import Optional import typer -from rich.console import Console - from moss import MossClient +from rich.console import Console from .. import output from ..completion import complete_index_name @@ -32,13 +30,13 @@ def create( ctx: typer.Context, name: str = typer.Argument(..., help="Index name"), file: str = typer.Option(..., "--file", "-f", help="Path to JSON/CSV document file, or '-' for stdin"), - model: Optional[str] = typer.Option(None, "--model", "-m", help="Model ID (default: moss-minilm)"), - profile: Optional[str] = typer.Option( + model: str | None = typer.Option(None, "--model", "-m", help="Model ID (default: moss-minilm)"), + profile: str | None = typer.Option( None, "--profile", help="Credential profile name" ), wait: bool = typer.Option(False, "--wait", "-w", help="Wait for job to complete"), poll_interval: float = typer.Option(2.0, "--poll-interval", help="Seconds between status checks"), - timeout: Optional[float] = typer.Option(None, "--timeout", help="Max seconds to wait (requires --wait)"), + timeout: float | None = typer.Option(None, "--timeout", help="Max seconds to wait (requires --wait)"), ) -> None: """Create a new index with documents.""" json_mode = ctx.obj.get("json_output", False) @@ -60,7 +58,7 @@ def create( @index_app.command(name="list") def list_indexes( ctx: typer.Context, - profile: Optional[str] = typer.Option( + profile: str | None = typer.Option( None, "--profile", help="Credential profile name" ), ) -> None: @@ -77,7 +75,7 @@ def list_indexes( def get( ctx: typer.Context, name: str = typer.Argument(..., help="Index name", autocompletion=complete_index_name), - profile: Optional[str] = typer.Option( + profile: str | None = typer.Option( None, "--profile", help="Credential profile name" ), ) -> None: @@ -94,7 +92,7 @@ def get( def delete( ctx: typer.Context, name: str = typer.Argument(..., help="Index name", autocompletion=complete_index_name), - profile: Optional[str] = typer.Option( + profile: str | None = typer.Option( None, "--profile", help="Credential profile name" ), confirm: bool = typer.Option(False, "--confirm", "-y", help="Skip confirmation"), diff --git a/packages/moss-cli/src/moss_cli/commands/init_cmd.py b/packages/moss-cli/src/moss_cli/commands/init_cmd.py index e060ce5a..ef2ba8fb 100644 --- a/packages/moss-cli/src/moss_cli/commands/init_cmd.py +++ b/packages/moss-cli/src/moss_cli/commands/init_cmd.py @@ -3,14 +3,12 @@ from __future__ import annotations import asyncio -from typing import Optional import typer +from moss import MossClient from rich.console import Console from rich.prompt import Prompt -from moss import MossClient - from ..config import ( get_config_path, get_profile_credentials, @@ -24,7 +22,7 @@ def init_command( ctx: typer.Context, force: bool = typer.Option(False, "--force", help="Overwrite existing config"), - profile: Optional[str] = typer.Option( + profile: str | None = typer.Option( None, "--profile", help="Profile name to save credentials under" ), ) -> None: diff --git a/packages/moss-cli/src/moss_cli/commands/job.py b/packages/moss-cli/src/moss_cli/commands/job.py index 5491cc97..6c4e15e4 100644 --- a/packages/moss-cli/src/moss_cli/commands/job.py +++ b/packages/moss-cli/src/moss_cli/commands/job.py @@ -5,7 +5,6 @@ import asyncio import typer - from moss import MossClient from .. import output diff --git a/packages/moss-cli/src/moss_cli/commands/search.py b/packages/moss-cli/src/moss_cli/commands/search.py index 5d241656..4f3d3442 100644 --- a/packages/moss-cli/src/moss_cli/commands/search.py +++ b/packages/moss-cli/src/moss_cli/commands/search.py @@ -5,12 +5,10 @@ import asyncio import json import sys -from typing import Optional import typer -from rich.console import Console - from moss import MossClient, QueryOptions +from rich.console import Console from .. import output from ..completion import complete_index_name @@ -19,7 +17,7 @@ console = Console() -def _parse_set_command(line: str) -> tuple[Optional[str], Optional[str]]: +def _parse_set_command(line: str) -> tuple[str | None, str | None]: parts = line.strip().split() if len(parts) != 3 or parts[0] != "/set": return None, "Usage: /set " @@ -49,13 +47,13 @@ def _parse_set_command(line: str) -> tuple[Optional[str], Optional[str]]: def query_command( ctx: typer.Context, index_name: str = typer.Argument(..., help="Index name", autocompletion=complete_index_name), - query_text: Optional[str] = typer.Argument(None, help="Search query (reads from stdin if omitted)"), - profile: Optional[str] = typer.Option( + query_text: str | None = typer.Argument(None, help="Search query (reads from stdin if omitted)"), + profile: str | None = typer.Option( None, "--profile", help="Credential profile name" ), top_k: int = typer.Option(10, "--top-k", "-k", help="Number of results"), alpha: float = typer.Option(0.8, "--alpha", "-a", help="Semantic weight (0.0=keyword, 1.0=semantic)"), - filter_json: Optional[str] = typer.Option(None, "--filter", help="Metadata filter as JSON string"), + filter_json: str | None = typer.Option(None, "--filter", help="Metadata filter as JSON string"), cloud: bool = typer.Option(False, "--cloud", "-c", help="Query via cloud API instead of downloading the index"), interactive: bool = typer.Option( False, diff --git a/packages/moss-cli/src/moss_cli/commands/sync.py b/packages/moss-cli/src/moss_cli/commands/sync.py index 37db5520..d3b82d28 100644 --- a/packages/moss-cli/src/moss_cli/commands/sync.py +++ b/packages/moss-cli/src/moss_cli/commands/sync.py @@ -4,12 +4,10 @@ import asyncio from pathlib import Path -from typing import Dict, Optional, Set import typer -from rich.console import Console - from moss import MossClient, MutationOptions +from rich.console import Console from .. import output from ..completion import complete_index_name @@ -27,9 +25,9 @@ def _client(ctx: typer.Context) -> MossClient: return MossClient(pid, pkey) -def _scan(directory: Path, exts: Set[str]) -> Dict[Path, float]: +def _scan(directory: Path, exts: set[str]) -> dict[Path, float]: """Return {path: mtime} for all files matching the given extensions.""" - result: Dict[Path, float] = {} + result: dict[Path, float] = {} for ext in exts: for p in directory.glob(f"**/*.{ext}"): result[p] = p.stat().st_mtime @@ -43,7 +41,7 @@ async def _upsert_file( json_mode: bool, wait: bool, poll_interval: float, - timeout: Optional[float], + timeout: float | None, ) -> None: try: docs = load_documents(str(path)) @@ -73,8 +71,8 @@ def sync_command( scan_interval: float = typer.Option(2.0, "--scan-interval", help="Seconds between directory scans (watch mode)"), wait: bool = typer.Option(False, "--wait", help="Wait for each upsert job to complete"), poll_interval: float = typer.Option(2.0, "--poll-interval", help="Seconds between job status checks"), - timeout: Optional[float] = typer.Option(None, "--timeout", help="Max seconds to wait per job (requires --wait)"), - profile: Optional[str] = typer.Option(None, "--profile", help="Credential profile name"), + timeout: float | None = typer.Option(None, "--timeout", help="Max seconds to wait per job (requires --wait)"), + profile: str | None = typer.Option(None, "--profile", help="Credential profile name"), ) -> None: """Sync documents from a directory into an index. diff --git a/packages/moss-cli/src/moss_cli/commands/validate.py b/packages/moss-cli/src/moss_cli/commands/validate.py index 154ef781..3ddc710a 100644 --- a/packages/moss-cli/src/moss_cli/commands/validate.py +++ b/packages/moss-cli/src/moss_cli/commands/validate.py @@ -4,7 +4,6 @@ import json from collections import Counter -from typing import List import typer from rich.console import Console @@ -34,7 +33,7 @@ def validate_command( _report(json_mode, valid=False, doc_count=0, issues=[str(e)]) raise typer.Exit(1) - issues: List[str] = [] + issues: list[str] = [] if not docs: issues.append("File contains 0 documents") @@ -60,7 +59,7 @@ def validate_command( raise typer.Exit(1) -def _report(json_mode: bool, valid: bool, doc_count: int, issues: List[str]) -> None: +def _report(json_mode: bool, valid: bool, doc_count: int, issues: list[str]) -> None: if json_mode: print(json.dumps({"valid": valid, "doc_count": doc_count, "issues": issues}, indent=2)) return diff --git a/packages/moss-cli/src/moss_cli/commands/version.py b/packages/moss-cli/src/moss_cli/commands/version.py index 5e3c61a9..b0bce351 100644 --- a/packages/moss-cli/src/moss_cli/commands/version.py +++ b/packages/moss-cli/src/moss_cli/commands/version.py @@ -10,9 +10,10 @@ def version_command(ctx: typer.Context) -> None: """Print CLI and SDK version information.""" - import moss_cli import moss + import moss_cli + json_mode = ctx.obj.get("json_output", False) if json_mode: diff --git a/packages/moss-cli/src/moss_cli/completion.py b/packages/moss-cli/src/moss_cli/completion.py index b62c526a..5b81bb7a 100644 --- a/packages/moss-cli/src/moss_cli/completion.py +++ b/packages/moss-cli/src/moss_cli/completion.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio -from typing import List import typer @@ -11,8 +10,8 @@ def complete_index_name( - ctx: typer.Context, args: List[str], incomplete: str -) -> List[str]: + ctx: typer.Context, args: list[str], incomplete: str +) -> list[str]: """Autocompletion callback that lists the user's index names. The shell invokes this while completing an index-name argument. It resolves diff --git a/packages/moss-cli/src/moss_cli/config.py b/packages/moss-cli/src/moss_cli/config.py index 8929434e..eadd228f 100644 --- a/packages/moss-cli/src/moss_cli/config.py +++ b/packages/moss-cli/src/moss_cli/config.py @@ -5,11 +5,9 @@ import json import os from pathlib import Path -from typing import Optional, Tuple import typer - DEFAULT_PROFILE = "default" @@ -69,7 +67,7 @@ def _normalize_config(config: dict) -> dict: return normalized -def get_selected_profile(profile: Optional[str] = None) -> str: +def get_selected_profile(profile: str | None = None) -> str: if profile: return profile @@ -84,7 +82,7 @@ def get_selected_profile(profile: Optional[str] = None) -> str: return DEFAULT_PROFILE -def get_profile_credentials(profile: str) -> Tuple[Optional[str], Optional[str]]: +def get_profile_credentials(profile: str) -> tuple[str | None, str | None]: config = _normalize_config(load_config()) profiles = config.get("profiles", {}) if not isinstance(profiles, dict): @@ -108,9 +106,7 @@ def set_profile_credentials(profile: str, project_id: str, project_key: str) -> profiles[profile] = {"project_id": project_id, "project_key": project_key} active_profile = normalized.get("active_profile") - if not isinstance(active_profile, str) or not active_profile: - active_profile = profile - elif active_profile not in profiles: + if not isinstance(active_profile, str) or not active_profile or active_profile not in profiles: active_profile = profile save_config({"active_profile": active_profile, "profiles": profiles}) @@ -120,10 +116,10 @@ def list_profiles() -> list[str]: profiles = config.get("profiles", {}) if not isinstance(profiles, dict): return [] - return sorted(name for name in profiles.keys() if isinstance(name, str) and name) + return sorted(name for name in profiles if isinstance(name, str) and name) -def delete_profile(profile: str) -> Tuple[bool, Optional[str]]: +def delete_profile(profile: str) -> tuple[bool, str | None]: normalized = _normalize_config(load_config()) profiles = normalized.get("profiles", {}) if not isinstance(profiles, dict) or profile not in profiles: @@ -132,7 +128,7 @@ def delete_profile(profile: str) -> Tuple[bool, Optional[str]]: del profiles[profile] active_profile = normalized.get("active_profile") - remaining_profiles = sorted(name for name in profiles.keys() if isinstance(name, str) and name) + remaining_profiles = sorted(name for name in profiles if isinstance(name, str) and name) if ( not isinstance(active_profile, str) or not active_profile @@ -149,10 +145,10 @@ def delete_profile(profile: str) -> Tuple[bool, Optional[str]]: def resolve_credentials( - project_id: Optional[str] = None, - project_key: Optional[str] = None, - profile: Optional[str] = None, -) -> Tuple[str, str]: + project_id: str | None = None, + project_key: str | None = None, + profile: str | None = None, +) -> tuple[str, str]: """Resolve credentials from flags, env vars, or config file.""" pid = project_id or os.getenv("MOSS_PROJECT_ID") pkey = project_key or os.getenv("MOSS_PROJECT_KEY") diff --git a/packages/moss-cli/src/moss_cli/documents.py b/packages/moss-cli/src/moss_cli/documents.py index 9c0631a3..22dc6a77 100644 --- a/packages/moss-cli/src/moss_cli/documents.py +++ b/packages/moss-cli/src/moss_cli/documents.py @@ -6,13 +6,13 @@ import json import sys from pathlib import Path -from typing import Any, List +from typing import Any import typer from moss import DocumentInfo -def load_documents(file_path: str) -> List[DocumentInfo]: +def load_documents(file_path: str) -> list[DocumentInfo]: """Load documents from a JSON/CSV file or stdin ('-').""" if file_path == "-": raw = sys.stdin.read() @@ -35,7 +35,7 @@ def load_documents(file_path: str) -> List[DocumentInfo]: return _parse_json_docs(content, source=file_path) -def _parse_json_docs(raw: str, source: str = "input") -> List[DocumentInfo]: +def _parse_json_docs(raw: str, source: str = "input") -> list[DocumentInfo]: try: data = json.loads(raw) except json.JSONDecodeError as e: @@ -52,7 +52,7 @@ def _parse_json_docs(raw: str, source: str = "input") -> List[DocumentInfo]: return [_dict_to_doc(d, i) for i, d in enumerate(data)] -def _parse_jsonl_docs(raw: str, source: str = "input") -> List[DocumentInfo]: +def _parse_jsonl_docs(raw: str, source: str = "input") -> list[DocumentInfo]: docs = [] for line_no, line in enumerate(raw.splitlines(), start=1): line = line.strip() @@ -66,7 +66,7 @@ def _parse_jsonl_docs(raw: str, source: str = "input") -> List[DocumentInfo]: return docs -def _parse_csv_docs(content: str) -> List[DocumentInfo]: +def _parse_csv_docs(content: str) -> list[DocumentInfo]: reader = csv.DictReader(content.splitlines()) docs = [] for i, row in enumerate(reader): @@ -75,7 +75,7 @@ def _parse_csv_docs(content: str) -> List[DocumentInfo]: f"CSV row {i + 1}: missing required 'id' or 'text' column" ) metadata = None - if "metadata" in row and row["metadata"]: + if row.get("metadata"): try: metadata = json.loads(row["metadata"]) except json.JSONDecodeError: @@ -84,7 +84,7 @@ def _parse_csv_docs(content: str) -> List[DocumentInfo]: ) embedding = None - if "embedding" in row and row["embedding"]: + if row.get("embedding"): try: embedding = json.loads(row["embedding"]) except json.JSONDecodeError: diff --git a/packages/moss-cli/src/moss_cli/job_waiter.py b/packages/moss-cli/src/moss_cli/job_waiter.py index 2f82e4cb..e4d03215 100644 --- a/packages/moss-cli/src/moss_cli/job_waiter.py +++ b/packages/moss-cli/src/moss_cli/job_waiter.py @@ -6,15 +6,13 @@ import json import sys import time -from typing import Optional +from moss import MossClient from rich.console import Console from rich.live import Live from rich.spinner import Spinner from rich.text import Text -from moss import MossClient - from . import output console = Console() @@ -44,7 +42,7 @@ async def wait_for_job( job_id: str, poll_interval: float = 2.0, json_mode: bool = False, - timeout: Optional[float] = None, + timeout: float | None = None, ) -> None: """Poll job status until terminal state, showing progress.""" terminal = {"COMPLETED", "FAILED"} diff --git a/packages/moss-cli/src/moss_cli/main.py b/packages/moss-cli/src/moss_cli/main.py index eae0ef50..e9430e5f 100644 --- a/packages/moss-cli/src/moss_cli/main.py +++ b/packages/moss-cli/src/moss_cli/main.py @@ -3,7 +3,6 @@ from __future__ import annotations import logging -from typing import Optional import typer @@ -44,13 +43,13 @@ @app.callback() def main( ctx: typer.Context, - project_id: Optional[str] = typer.Option( + project_id: str | None = typer.Option( None, "--project-id", "-p", envvar="MOSS_PROJECT_ID", help="Project ID" ), - project_key: Optional[str] = typer.Option( + project_key: str | None = typer.Option( None, "--project-key", envvar="MOSS_PROJECT_KEY", help="Project key" ), - profile: Optional[str] = typer.Option( + profile: str | None = typer.Option( None, "--profile", envvar="MOSS_PROFILE", diff --git a/packages/moss-cli/src/moss_cli/output.py b/packages/moss-cli/src/moss_cli/output.py index d89aee36..4f9aa071 100644 --- a/packages/moss-cli/src/moss_cli/output.py +++ b/packages/moss-cli/src/moss_cli/output.py @@ -4,7 +4,7 @@ import json import sys -from typing import Any, Dict +from typing import Any from rich.console import Console from rich.table import Table @@ -13,7 +13,7 @@ err_console = Console(stderr=True) -def _index_to_dict(info: Any) -> Dict[str, Any]: +def _index_to_dict(info: Any) -> dict[str, Any]: return { "id": info.id, "name": info.name, @@ -26,8 +26,8 @@ def _index_to_dict(info: Any) -> Dict[str, Any]: } -def _doc_to_dict(doc: Any) -> Dict[str, Any]: - d: Dict[str, Any] = {"id": doc.id, "text": doc.text} +def _doc_to_dict(doc: Any) -> dict[str, Any]: + d: dict[str, Any] = {"id": doc.id, "text": doc.text} meta = getattr(doc, "metadata", None) if meta is not None: d["metadata"] = dict(meta) @@ -37,15 +37,15 @@ def _doc_to_dict(doc: Any) -> Dict[str, Any]: return d -def _result_doc_to_dict(doc: Any) -> Dict[str, Any]: - d: Dict[str, Any] = {"id": doc.id, "text": doc.text, "score": doc.score} +def _result_doc_to_dict(doc: Any) -> dict[str, Any]: + d: dict[str, Any] = {"id": doc.id, "text": doc.text, "score": doc.score} meta = getattr(doc, "metadata", None) if meta is not None: d["metadata"] = dict(meta) return d -def _search_result_to_dict(result: Any) -> Dict[str, Any]: +def _search_result_to_dict(result: Any) -> dict[str, Any]: return { "query": result.query, "index_name": result.index_name, @@ -54,7 +54,7 @@ def _search_result_to_dict(result: Any) -> Dict[str, Any]: } -def _mutation_to_dict(result: Any) -> Dict[str, Any]: +def _mutation_to_dict(result: Any) -> dict[str, Any]: return { "job_id": result.job_id, "index_name": result.index_name, @@ -62,8 +62,8 @@ def _mutation_to_dict(result: Any) -> Dict[str, Any]: } -def _job_status_to_dict(status: Any) -> Dict[str, Any]: - d: Dict[str, Any] = { +def _job_status_to_dict(status: Any) -> dict[str, Any]: + d: dict[str, Any] = { "job_id": status.job_id, "status": status.status.value if hasattr(status.status, "value") else str(status.status), "progress": status.progress, diff --git a/packages/moss-cli/tests/test_completions.py b/packages/moss-cli/tests/test_completions.py index b7b2c185..3ecd77a3 100644 --- a/packages/moss-cli/tests/test_completions.py +++ b/packages/moss-cli/tests/test_completions.py @@ -1,7 +1,6 @@ -from typer.testing import CliRunner - from moss_cli import completion from moss_cli.main import app +from typer.testing import CliRunner runner = CliRunner() diff --git a/packages/moss-cli/tests/test_profiles.py b/packages/moss-cli/tests/test_profiles.py index 5a1ce53e..4d920f67 100644 --- a/packages/moss-cli/tests/test_profiles.py +++ b/packages/moss-cli/tests/test_profiles.py @@ -1,13 +1,11 @@ import json -from typer.testing import CliRunner - from moss_cli import config -from moss_cli.commands import init_cmd from moss_cli.commands import index as index_cmd +from moss_cli.commands import init_cmd from moss_cli.commands import search as search_cmd from moss_cli.main import app - +from typer.testing import CliRunner runner = CliRunner() diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 00000000..d08567c6 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,40 @@ +# Repo-wide ruff settings for directories without their own ruff config +# (packages that define [tool.ruff] in pyproject.toml, or their own ruff.toml, +# keep using it). +target-version = "py310" +# Notebooks are not linted in CI (see `--extend-exclude` in ci.yml). +extend-exclude = ["*.ipynb"] + +[lint] +# Imports placed after executable setup (e.g. load_dotenv) must be marked with +# `# noqa: E402` so the ordering is deliberate and reviewed. +extend-select = ["E402"] +extend-ignore = [ + # Examples, voice agents, and E2E test cleanup deliberately catch broad + # exceptions so a failed retrieval or teardown never crashes the demo/run. + "BLE001", # blind-except + "S110", # try-except-pass + # `logger.error(..., exc_info=True)` is equivalent to `logger.exception`. + "G201", # logging-exc-info + # Validation of user input / loaded data raises ValueError by design. + "TRY004", # type-check-without-type-error + # Langflow and smolagents components declare `inputs`/`outputs` as + # class-level lists/dicts as part of their public API. + "RUF012", # mutable-class-default +] + +[lint.per-file-ignores] +# These tests import sibling modules after `sys.path.insert(...)`. Ruff already +# exempts imports that follow sys.path edits, so an inline `# noqa: E402` would +# be reported as unused (RUF100); the ignore documents the intent here instead. +"examples/voice-agents/airline-pnr/evals/test_call_summary.py" = ["E402"] +"examples/voice-agents/candidate-screening/evals/test_scorecards.py" = ["E402"] +"examples/cookbook/langgraph/test_integration.py" = ["E402"] + +[lint.flake8-bugbear] +# Declarative CLI/HTTP parameter defaults, not shared mutable state. +extend-immutable-calls = [ + "fastapi.File", + "typer.Argument", + "typer.Option", +] diff --git a/sdks/python/sdk/src/moss/__init__.py b/sdks/python/sdk/src/moss/__init__.py index 13e3ef2d..0a349a0c 100644 --- a/sdks/python/sdk/src/moss/__init__.py +++ b/sdks/python/sdk/src/moss/__init__.py @@ -24,13 +24,13 @@ IndexInfo, IndexStatus, IndexStatusValues, - ModelRef, - MutationOptions, - MutationResult, - JobStatus, JobPhase, JobProgress, + JobStatus, JobStatusResponse, + ModelRef, + MutationOptions, + MutationResult, QueryOptions, QueryResultDocumentInfo, SearchResult, @@ -41,22 +41,20 @@ __version__ = "1.0.0b19" __all__ = [ - "MossClient", - # Core data types "DocumentInfo", "GetDocumentsOptions", "IndexInfo", - "SearchResult", - "QueryResultDocumentInfo", - "ModelRef", "IndexStatus", "IndexStatusValues", - "QueryOptions", - # Mutation types - "MutationResult", - "MutationOptions", - "JobStatus", "JobPhase", "JobProgress", + "JobStatus", "JobStatusResponse", + "ModelRef", + "MossClient", + "MutationOptions", + "MutationResult", + "QueryOptions", + "QueryResultDocumentInfo", + "SearchResult", ] diff --git a/sdks/python/sdk/src/moss/__init__.pyi b/sdks/python/sdk/src/moss/__init__.pyi index 6aa41699..6fc3ec8a 100644 --- a/sdks/python/sdk/src/moss/__init__.pyi +++ b/sdks/python/sdk/src/moss/__init__.pyi @@ -1,7 +1,6 @@ -from __future__ import annotations - -from typing import ClassVar, Dict, List, Optional, Sequence +from collections.abc import Sequence +from typing import ClassVar class MossClient: """Semantic search client for vector similarity operations.""" @@ -13,36 +12,36 @@ class MossClient: async def create_index( self, name: str, - docs: List[DocumentInfo], - model_id: Optional[str] = ..., + docs: list[DocumentInfo], + model_id: str | None = ..., ) -> MutationResult: ... async def add_docs( self, name: str, - docs: List[DocumentInfo], - options: Optional[MutationOptions] = None, + docs: list[DocumentInfo], + options: MutationOptions | None = None, ) -> MutationResult: ... async def delete_docs( self, name: str, - doc_ids: List[str], + doc_ids: list[str], ) -> MutationResult: ... async def get_job_status(self, job_id: str) -> JobStatusResponse: ... async def get_index(self, name: str) -> IndexInfo: ... - async def list_indexes(self) -> List[IndexInfo]: ... + async def list_indexes(self) -> list[IndexInfo]: ... async def delete_index(self, name: str) -> bool: ... async def get_docs( self, name: str, - options: Optional[GetDocumentsOptions] = None, - ) -> List[DocumentInfo]: ... + options: GetDocumentsOptions | None = None, + ) -> list[DocumentInfo]: ... async def load_index( self, @@ -57,7 +56,7 @@ class MossClient: self, name: str, query: str, - options: Optional[QueryOptions] = None, + options: QueryOptions | None = None, ) -> SearchResult: ... @@ -72,17 +71,17 @@ class MutationResult: class MutationOptions: """Options for add_docs (e.g. upsert behavior).""" - upsert: Optional[bool] + upsert: bool | None - def __init__(self, upsert: Optional[bool] = None) -> None: ... + def __init__(self, upsert: bool | None = None) -> None: ... class GetDocumentsOptions: """Options for get_docs (e.g. filter by document IDs).""" - doc_ids: Optional[List[str]] + doc_ids: list[str] | None - def __init__(self, doc_ids: Optional[List[str]] = None) -> None: ... + def __init__(self, doc_ids: list[str] | None = None) -> None: ... class JobStatus: @@ -116,7 +115,7 @@ class JobProgress: job_id: str status: JobStatus progress: float - current_phase: Optional[JobPhase] + current_phase: JobPhase | None class JobStatusResponse: @@ -125,11 +124,11 @@ class JobStatusResponse: job_id: str status: JobStatus progress: float - current_phase: Optional[JobPhase] - error: Optional[str] + current_phase: JobPhase | None + error: str | None created_at: str updated_at: str - completed_at: Optional[str] + completed_at: str | None class ModelRef: @@ -141,13 +140,13 @@ class ModelRef: class QueryResultDocumentInfo: id: str text: str - metadata: Optional[Dict[str, str]] + metadata: dict[str, str] | None score: float def __init__( self, id: str, text: str, - metadata: Optional[Dict[str, str]] = ..., + metadata: dict[str, str] | None = ..., score: float = ..., ) -> None: ... @@ -155,28 +154,28 @@ class QueryResultDocumentInfo: class DocumentInfo: id: str text: str - metadata: Optional[Dict[str, str]] - embedding: Optional[Sequence[float]] + metadata: dict[str, str] | None + embedding: Sequence[float] | None def __init__( self, id: str, text: str, - metadata: Optional[Dict[str, str]] = ..., - embedding: Optional[Sequence[float]] = ..., + metadata: dict[str, str] | None = ..., + embedding: Sequence[float] | None = ..., ) -> None: ... class QueryOptions: - embedding: Optional[Sequence[float]] - top_k: Optional[int] - alpha: Optional[float] - filter: Optional[dict] + embedding: Sequence[float] | None + top_k: int | None + alpha: float | None + filter: dict | None def __init__( self, - embedding: Optional[Sequence[float]] = ..., - top_k: Optional[int] = ..., - alpha: Optional[float] = ..., - filter: Optional[dict] = ..., + embedding: Sequence[float] | None = ..., + top_k: int | None = ..., + alpha: float | None = ..., + filter: dict | None = ..., ) -> None: ... @@ -203,16 +202,16 @@ class IndexInfo: class SearchResult: - docs: List[QueryResultDocumentInfo] + docs: list[QueryResultDocumentInfo] query: str - index_name: Optional[str] - time_taken_ms: Optional[int] + index_name: str | None + time_taken_ms: int | None def __init__( self, - docs: List[QueryResultDocumentInfo], + docs: list[QueryResultDocumentInfo], query: str, - index_name: Optional[str] = None, - time_taken_ms: Optional[int] = None, + index_name: str | None = None, + time_taken_ms: int | None = None, ) -> None: ... @@ -224,25 +223,25 @@ class IndexStatus: def __init__(self, value: str) -> None: ... -IndexStatusValues: Dict[str, str] +IndexStatusValues: dict[str, str] __version__: str __all__ = [ - "MossClient", "DocumentInfo", "GetDocumentsOptions", "IndexInfo", - "SearchResult", - "QueryResultDocumentInfo", - "ModelRef", "IndexStatus", "IndexStatusValues", - "QueryOptions", - "MutationResult", - "MutationOptions", - "JobStatus", "JobPhase", "JobProgress", + "JobStatus", "JobStatusResponse", + "ModelRef", + "MossClient", + "MutationOptions", + "MutationResult", + "QueryOptions", + "QueryResultDocumentInfo", + "SearchResult", ] diff --git a/sdks/python/sdk/src/moss/client/moss_client.py b/sdks/python/sdk/src/moss/client/moss_client.py index 51bd2833..a26a00ee 100644 --- a/sdks/python/sdk/src/moss/client/moss_client.py +++ b/sdks/python/sdk/src/moss/client/moss_client.py @@ -4,19 +4,19 @@ import logging import os import uuid -from typing import Any, Dict, List, Optional +from typing import Any import httpx from moss_core import ( CLOUD_API_MANAGE_URL, - ManageClient, DocumentInfo, GetDocumentsOptions, IndexInfo, IndexManager, + JobStatusResponse, + ManageClient, MutationOptions, MutationResult, - JobStatusResponse, QueryOptions, QueryResultDocumentInfo, SearchResult, @@ -79,8 +79,8 @@ def __init__(self, project_id: str, project_key: str) -> None: async def create_index( self, name: str, - docs: List[DocumentInfo], - model_id: Optional[str] = None, + docs: list[DocumentInfo], + model_id: str | None = None, ) -> MutationResult: """Create a new index and populate it with documents.""" resolved_model_id = self._resolve_model_id(docs, model_id) @@ -94,8 +94,8 @@ async def create_index( async def add_docs( self, name: str, - docs: List[DocumentInfo], - options: Optional[MutationOptions] = None, + docs: list[DocumentInfo], + options: MutationOptions | None = None, ) -> MutationResult: """Add or update documents in an index.""" return await asyncio.to_thread( @@ -108,7 +108,7 @@ async def add_docs( async def delete_docs( self, name: str, - doc_ids: List[str], + doc_ids: list[str], ) -> MutationResult: """Delete documents from an index by their IDs.""" return await asyncio.to_thread( @@ -127,7 +127,7 @@ async def get_index(self, name: str) -> IndexInfo: """Get information about a specific index.""" return await asyncio.to_thread(self._manage.get_index, name) - async def list_indexes(self) -> List[IndexInfo]: + async def list_indexes(self) -> list[IndexInfo]: """List all indexes with their information.""" return await asyncio.to_thread(self._manage.list_indexes) @@ -138,8 +138,8 @@ async def delete_index(self, name: str) -> bool: async def get_docs( self, name: str, - options: Optional[GetDocumentsOptions] = None, - ) -> List[DocumentInfo]: + options: GetDocumentsOptions | None = None, + ) -> list[DocumentInfo]: """Retrieve documents from an index.""" return await asyncio.to_thread(self._manage.get_docs, name, options) @@ -180,7 +180,7 @@ async def query( self, name: str, query: str, - options: Optional[QueryOptions] = None, + options: QueryOptions | None = None, ) -> SearchResult: """ Perform a semantic similarity search. @@ -214,7 +214,7 @@ async def _query_local( self, name: str, query: str, - options: Optional[QueryOptions], + options: QueryOptions | None, ) -> SearchResult: top_k = getattr(options, "top_k", None) if top_k is None: @@ -257,13 +257,13 @@ async def _query_cloud( self, name: str, query: str, - options: Optional[QueryOptions], + options: QueryOptions | None, ) -> SearchResult: """Fallback: query via the cloud API when the index is not loaded locally.""" top_k = getattr(options, "top_k", None) or 10 query_embedding = getattr(options, "embedding", None) - request_body: Dict[str, Any] = { + request_body: dict[str, Any] = { "query": query, "indexName": name, "projectId": self._project_id, @@ -281,10 +281,10 @@ async def _query_cloud( json=request_body, ) if not response.is_success: - raise Exception(f"HTTP error! status: {response.status_code}") + raise RuntimeError(f"HTTP error! status: {response.status_code}") data = response.json() except httpx.RequestError as error: - raise Exception(f"Cloud query request failed: {str(error)}") + raise RuntimeError(f"Cloud query request failed: {error!s}") from error return self._dict_to_search_result(data) @@ -308,8 +308,8 @@ def _dict_to_search_result(data: dict) -> SearchResult: def _resolve_model_id( self, - docs: List[DocumentInfo], - model_id: Optional[str], + docs: list[DocumentInfo], + model_id: str | None, ) -> str: if model_id is not None: return model_id diff --git a/sdks/python/sdk/tests/conftest.py b/sdks/python/sdk/tests/conftest.py index 28651233..7451c7d9 100644 --- a/sdks/python/sdk/tests/conftest.py +++ b/sdks/python/sdk/tests/conftest.py @@ -6,13 +6,11 @@ import os import warnings from pathlib import Path - from unittest.mock import MagicMock, patch -from moss import MossClient - import pytest from dotenv import load_dotenv +from moss import MossClient # Load .env from project root. project_env_path = Path(__file__).parent.parent / ".env" @@ -38,9 +36,7 @@ def _has_real_cloud_creds() -> bool: project_key = os.getenv("MOSS_TEST_PROJECT_KEY", "") if not project_id or not project_key: return False - if project_id == "test-project-id" or project_key == "test-project-key": - return False - return True + return not (project_id == "test-project-id" or project_key == "test-project-key") def pytest_configure(config): diff --git a/sdks/python/sdk/tests/test_client.py b/sdks/python/sdk/tests/test_client.py index 86c1f2fc..b4e5e923 100644 --- a/sdks/python/sdk/tests/test_client.py +++ b/sdks/python/sdk/tests/test_client.py @@ -5,7 +5,6 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest - from moss import MossClient @@ -24,7 +23,7 @@ def raw_mocks(): class TestConstructor: def test_manage_client_created_with_manage_url(self, raw_mocks): - mock_manage_cls, mock_mgr_cls = raw_mocks + mock_manage_cls, _ = raw_mocks MossClient("pid", "pkey") mock_manage_cls.assert_called_once() @@ -34,7 +33,7 @@ def test_manage_client_created_with_manage_url(self, raw_mocks): assert "/v1/manage" in args[2] def test_index_manager_created_with_manage_url(self, raw_mocks): - mock_manage_cls, mock_mgr_cls = raw_mocks + _, mock_mgr_cls = raw_mocks MossClient("pid", "pkey") mock_mgr_cls.assert_called_once() diff --git a/sdks/python/sdk/tests/test_client_extended.py b/sdks/python/sdk/tests/test_client_extended.py index 2b53f916..ab8ab793 100644 --- a/sdks/python/sdk/tests/test_client_extended.py +++ b/sdks/python/sdk/tests/test_client_extended.py @@ -2,14 +2,12 @@ from __future__ import annotations +from importlib.metadata import version from unittest.mock import AsyncMock, MagicMock, patch import httpx import pytest - -from importlib.metadata import version - -from moss import __version__, MossClient +from moss import MossClient, __version__ from moss.client.moss_client import _get_manage_url, _get_query_url @@ -59,7 +57,7 @@ async def test_cloud_fallback_http_error(self, unloaded_client): ) mock_httpx.return_value.__aexit__ = AsyncMock(return_value=False) - with pytest.raises(Exception, match="HTTP error! status: 500"): + with pytest.raises(RuntimeError, match="HTTP error! status: 500"): await unloaded_client.query("idx", "test query") @pytest.mark.asyncio @@ -74,9 +72,13 @@ async def test_cloud_fallback_network_error(self, unloaded_client): ) mock_httpx.return_value.__aexit__ = AsyncMock(return_value=False) - with pytest.raises(Exception, match="Cloud query request failed"): + with pytest.raises( + RuntimeError, match="Cloud query request failed" + ) as exc_info: await unloaded_client.query("idx", "test query") + assert isinstance(exc_info.value.__cause__, httpx.RequestError) + @pytest.mark.asyncio async def test_cloud_fallback_with_custom_embedding(self, unloaded_client): mock_response = MagicMock() diff --git a/sdks/python/sdk/tests/test_cloud_fallback.py b/sdks/python/sdk/tests/test_cloud_fallback.py index 938465bc..54e8f3c7 100644 --- a/sdks/python/sdk/tests/test_cloud_fallback.py +++ b/sdks/python/sdk/tests/test_cloud_fallback.py @@ -20,8 +20,8 @@ import pytest import pytest_asyncio - from moss import DocumentInfo, MossClient, QueryOptions + from tests.constants import ( TEST_DOCUMENTS, TEST_MODEL_ID, diff --git a/sdks/python/sdk/tests/test_create_index_versions.py b/sdks/python/sdk/tests/test_create_index_versions.py index 9f81a71b..08b4f245 100644 --- a/sdks/python/sdk/tests/test_create_index_versions.py +++ b/sdks/python/sdk/tests/test_create_index_versions.py @@ -8,25 +8,25 @@ import string import time from dataclasses import dataclass -from importlib.metadata import PackageNotFoundError, version as pkg_version -from typing import List, Optional +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version as pkg_version import pytest - from moss import DocumentInfo, MossClient from .constants import TEST_MODEL_ID, TEST_PROJECT_ID, TEST_PROJECT_KEY + # Define the structure to hold benchmark results @dataclass class BenchResult: doc_count: int - time_ms: Optional[float] + time_ms: float | None success: bool error: str = "" # Generate dummy documents with random deterministic text of fixed length 200 -def _gen_docs(n: int, seed: int = 1337) -> List[DocumentInfo]: +def _gen_docs(n: int, seed: int = 1337) -> list[DocumentInfo]: """Generate n dummy documents.""" rng = random.Random(seed) alphabet = string.ascii_letters + " " diff --git a/sdks/python/sdk/tests/test_e2e.py b/sdks/python/sdk/tests/test_e2e.py index edbc375e..5a532664 100644 --- a/sdks/python/sdk/tests/test_e2e.py +++ b/sdks/python/sdk/tests/test_e2e.py @@ -16,18 +16,15 @@ import random import time -from typing import List import pytest - -from moss_core import DocumentInfo - from moss import GetDocumentsOptions, MossClient +from moss_core import DocumentInfo from .constants import ( + TEST_MODEL_ID, TEST_PROJECT_ID, TEST_PROJECT_KEY, - TEST_MODEL_ID, generate_unique_index_name, ) @@ -45,7 +42,7 @@ ] -def generate_docs(count: int, seed: int = 42) -> List[DocumentInfo]: +def generate_docs(count: int, seed: int = 42) -> list[DocumentInfo]: rng = random.Random(seed) docs = [] for i in range(count): @@ -59,7 +56,7 @@ def generate_docs(count: int, seed: int = 42) -> List[DocumentInfo]: def generate_docs_with_embeddings( count: int, dimension: int, seed: int = 42, -) -> List[DocumentInfo]: +) -> list[DocumentInfo]: rng = random.Random(seed) docs = [] for i in range(count): diff --git a/sdks/python/sdk/tests/test_hot_reload.py b/sdks/python/sdk/tests/test_hot_reload.py index 6649d951..c85178ff 100644 --- a/sdks/python/sdk/tests/test_hot_reload.py +++ b/sdks/python/sdk/tests/test_hot_reload.py @@ -5,12 +5,12 @@ import pytest import pytest_asyncio - from moss import ( DocumentInfo, MossClient, QueryOptions, ) + from tests.constants import ( TEST_DOCUMENTS, TEST_MODEL_ID, @@ -288,7 +288,7 @@ async def test_fail_to_load_non_existent_index(self, moss_client): """Should fail to load a non-existent index.""" non_existent_index_name = "non-existent-index-for-test" - with pytest.raises(Exception): + with pytest.raises(RuntimeError): await moss_client.load_index(non_existent_index_name) @pytest.mark.asyncio @@ -296,7 +296,7 @@ async def test_fail_to_query_non_existent_index(self, moss_client): """Should fail to query a non-existent index.""" non_existent_index_name = "non-existent-index-for-test" - with pytest.raises(Exception): + with pytest.raises(RuntimeError): await moss_client.query( non_existent_index_name, "test query", diff --git a/sdks/python/sdk/tests/test_metadata_filter_e2e.py b/sdks/python/sdk/tests/test_metadata_filter_e2e.py index 9b971aa2..61e99e38 100644 --- a/sdks/python/sdk/tests/test_metadata_filter_e2e.py +++ b/sdks/python/sdk/tests/test_metadata_filter_e2e.py @@ -17,8 +17,8 @@ import pytest import pytest_asyncio - from moss import DocumentInfo, MossClient, QueryOptions + from tests.constants import ( TEST_MODEL_ID, TEST_PROJECT_ID, diff --git a/sdks/python/sdk/tests/test_search.py b/sdks/python/sdk/tests/test_search.py index 10af17a5..8a1051fe 100644 --- a/sdks/python/sdk/tests/test_search.py +++ b/sdks/python/sdk/tests/test_search.py @@ -4,12 +4,13 @@ import statistics import time from dataclasses import dataclass +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version as pkg_version from pathlib import Path -from typing import Any, Dict, List, Optional, Set +from typing import Any -from importlib.metadata import PackageNotFoundError, version as pkg_version -from moss import MossClient, DocumentInfo, SearchResult, QueryOptions import pytest +from moss import DocumentInfo, MossClient, QueryOptions, SearchResult from .constants import TEST_PROJECT_ID, TEST_PROJECT_KEY @@ -22,7 +23,7 @@ class ExperimentConfig: name: str dataset_path: Path - alpha: Optional[float] + alpha: float | None top_k: int = 10 # Interface for Run Statistics @@ -32,25 +33,25 @@ class RunStats: sdk_version: str dataset_name: str num_queries: int - hit_rate: Optional[float] - mrr: Optional[float] - ndcg: Optional[float] - avg_latency_ms: Optional[float] - p95_latency_ms: Optional[float] - error_reason: Optional[str] = None + hit_rate: float | None + mrr: float | None + ndcg: float | None + avg_latency_ms: float | None + p95_latency_ms: float | None + error_reason: str | None = None # Dataclass for Caching the dataset to avoid re-loading @dataclass class DatasetCache: """Cache for dataset data to avoid re-loading across experiments.""" - dataset_path: Optional[Path] = None - qrels: Optional[Dict[str, Dict[str, float]]] = None - queries: Optional[Dict[str, str]] = None + dataset_path: Path | None = None + qrels: dict[str, dict[str, float]] | None = None + queries: dict[str, str] | None = None # -------------------- Data Loaders -------------------- #Loads the corpus from the corpus.jsonl file and combines the title and text into a single string -def load_corpus(path: Path) -> List[DocumentInfo]: +def load_corpus(path: Path) -> list[DocumentInfo]: """Loads the entire mini-corpus into memory at once.""" if not path.exists(): pytest.skip(f"File not found: {path}") @@ -72,7 +73,7 @@ def load_corpus(path: Path) -> List[DocumentInfo]: #-------- Loads the qrels from the qrels/test.tsv file -------- # Example struct of qrels : {"query-id": {"doc-id": score}} -def load_qrels(path: Path) -> Dict[str, Dict[str, float]]: +def load_qrels(path: Path) -> dict[str, dict[str, float]]: """Loads relevance (Query ID -> Doc ID -> Score).""" if not path.exists(): pytest.skip(f"Qrels file not found: {path}") @@ -121,7 +122,7 @@ def load_qrels(path: Path) -> Dict[str, Dict[str, float]]: #-------- Loads the queries from the queries.jsonl -------- # Example struct of queries : {"_id": "text"} -def load_queries(path: Path, valid_qids: Set[str]) -> Dict[str, str]: +def load_queries(path: Path, valid_qids: set[str]) -> dict[str, str]: """Loads queries that exist in the Qrels.""" queries = {} with path.open("r", encoding="utf-8") as f: @@ -143,7 +144,7 @@ def load_queries(path: Path, valid_qids: Set[str]) -> Dict[str, str]: # * Sums relevance gains (2^rel - 1) discounted by position (log2(i+2)) # - IDCG@k = DCG of the ideal ranking (documents sorted by relevance descending) # - Result: Score between 0.0 (worst) and 1.0 (perfect ranking) -def calculate_ndcg(retrieved_ids: List[str], true_rels: Dict[str, float], k: int) -> float: +def calculate_ndcg(retrieved_ids: list[str], true_rels: dict[str, float], k: int) -> float: dcg = 0.0 idcg = 0.0 @@ -162,7 +163,7 @@ def calculate_ndcg(retrieved_ids: List[str], true_rels: Dict[str, float], k: int # -------------------- Core Execution -------------------- # Run a single experiment scenario, based on the provided config -async def run_scenario(client: MossClient, config: ExperimentConfig, queries: Dict, qrels: Dict, index_name: str) -> RunStats: +async def run_scenario(client: MossClient, config: ExperimentConfig, queries: dict, qrels: dict, index_name: str) -> RunStats: print(f"\nRunning Experiment: {config.name} (alpha={config.alpha}, dataset={config.dataset_path.name}) \n") latencies = [] @@ -190,11 +191,10 @@ async def run_scenario(client: MossClient, config: ExperimentConfig, queries: Di # Check Hit & MRR is_hit = False for rank, doc_id in enumerate(retrieved_ids, 1): - if doc_id in relevant_docs: - if not is_hit: - hits += 1 - mrr_sum += 1.0 / rank - is_hit = True + if doc_id in relevant_docs and not is_hit: + hits += 1 + mrr_sum += 1.0 / rank + is_hit = True # Check NDCG ndcg_sum += calculate_ndcg(retrieved_ids, relevant_docs, config.top_k) @@ -260,11 +260,11 @@ async def main(): # PHASE 1: INDEX PREPARATION (Delete -> Create) # --------------------------------------------------------- # Identify all unique datasets required by the experiments - unique_dataset_paths = sorted(list({exp.dataset_path for exp in experiments})) + unique_dataset_paths = sorted({exp.dataset_path for exp in experiments}) # Map dataset paths to their created index names - dataset_to_index_map: Dict[Path, str] = {} - failed_datasets: Dict[Path, Dict[str, Any]] = {} + dataset_to_index_map: dict[Path, str] = {} + failed_datasets: dict[Path, dict[str, Any]] = {} # Get current version for safe index naming try: @@ -300,7 +300,7 @@ async def main(): # 4. Create Fresh Index print(f" ✨ Creating fresh index: {index_name}...") t0 = time.time() - skip_reason: Optional[str] = None + skip_reason: str | None = None try: success = await client.create_index(index_name, docs, EMBEDDING_MODEL) except Exception as exc: diff --git a/sdks/python/sdk/tests/test_types.py b/sdks/python/sdk/tests/test_types.py index f5694c28..96cb4d34 100644 --- a/sdks/python/sdk/tests/test_types.py +++ b/sdks/python/sdk/tests/test_types.py @@ -58,7 +58,7 @@ async def test_get_docs_with_doc_ids(self): patch("moss.client.moss_client.ManageClient") as mock_manage, patch("moss.client.moss_client.IndexManager") as _, ): - from moss import MossClient, GetDocumentsOptions + from moss import GetDocumentsOptions, MossClient mock_manage_instance = mock_manage.return_value mock_manage_instance.get_docs = MagicMock(return_value=[]) @@ -126,7 +126,7 @@ async def test_create_index_with_document_info(self): patch("moss.client.moss_client.ManageClient") as mock_manage, patch("moss.client.moss_client.IndexManager") as _, ): - from moss import MossClient, DocumentInfo + from moss import DocumentInfo, MossClient mock_manage_instance = mock_manage.return_value mock_result = MagicMock() @@ -154,7 +154,7 @@ async def test_create_index_with_metadata(self): patch("moss.client.moss_client.ManageClient") as mock_manage, patch("moss.client.moss_client.IndexManager") as _, ): - from moss import MossClient, DocumentInfo + from moss import DocumentInfo, MossClient mock_manage_instance = mock_manage.return_value mock_result = MagicMock() @@ -188,7 +188,7 @@ async def test_create_index_with_embeddings(self): patch("moss.client.moss_client.ManageClient") as mock_manage, patch("moss.client.moss_client.IndexManager") as _, ): - from moss import MossClient, DocumentInfo + from moss import DocumentInfo, MossClient mock_manage_instance = mock_manage.return_value mock_result = MagicMock() @@ -211,7 +211,7 @@ class TestSearchResultUsage: """Tests for SearchResult handling.""" def test_search_result_structure(self): - from moss import SearchResult, QueryResultDocumentInfo + from moss import QueryResultDocumentInfo, SearchResult doc1 = QueryResultDocumentInfo(id="d1", text="First", score=0.95) doc2 = QueryResultDocumentInfo(id="d2", text="Second", score=0.85)