bcbd1bdb22
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
181 lines
5.8 KiB
Python
181 lines
5.8 KiB
Python
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
|
|
|
import logging
|
|
import uuid
|
|
from typing import Any
|
|
|
|
from qdrant_client import models
|
|
from qdrant_client.http.exceptions import UnexpectedResponse
|
|
|
|
from mirage.accessor.qdrant import QdrantAccessor
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
SCROLL_BATCH = 256
|
|
|
|
|
|
def _coerce(value: str) -> Any:
|
|
if value.lstrip("-").isdigit():
|
|
as_int = int(value)
|
|
if str(as_int) == value:
|
|
return as_int
|
|
return value
|
|
|
|
|
|
def _filter(filters: dict[str, str]) -> models.Filter | None:
|
|
if not filters:
|
|
return None
|
|
return models.Filter(must=[
|
|
models.FieldCondition(key=column,
|
|
match=models.MatchValue(value=_coerce(value)))
|
|
for column, value in filters.items()
|
|
])
|
|
|
|
|
|
def _point_to_row(point: Any, id_field: str) -> dict:
|
|
payload = point.payload if isinstance(point.payload, dict) else {}
|
|
row = dict(payload)
|
|
row[id_field] = point.id
|
|
return row
|
|
|
|
|
|
def _candidate_ids(row_id: str) -> list[Any]:
|
|
if row_id.lstrip("-").isdigit():
|
|
return [int(row_id)]
|
|
try:
|
|
uuid.UUID(row_id)
|
|
except ValueError:
|
|
return []
|
|
return [row_id]
|
|
|
|
|
|
async def _scroll_raw(client: Any, collection: str, flt: models.Filter | None,
|
|
limit: int) -> list[Any]:
|
|
points: list[Any] = []
|
|
offset: Any = None
|
|
while len(points) < limit:
|
|
batch, offset = await client.scroll(
|
|
collection_name=collection,
|
|
scroll_filter=flt,
|
|
limit=min(SCROLL_BATCH, limit - len(points)),
|
|
offset=offset,
|
|
with_payload=True,
|
|
with_vectors=False,
|
|
)
|
|
points.extend(batch)
|
|
if offset is None:
|
|
break
|
|
return points[:limit]
|
|
|
|
|
|
def _is_index_required(exc: UnexpectedResponse) -> bool:
|
|
content = exc.content
|
|
text = content.decode() if isinstance(content, bytes) else str(content)
|
|
return exc.status_code == 400 and "index required" in text.lower()
|
|
|
|
|
|
async def _ensure_indexes(client: Any, accessor: QdrantAccessor,
|
|
collection: str) -> None:
|
|
if collection in accessor._indexes_ensured:
|
|
return
|
|
for field in accessor.config.group_by:
|
|
await client.create_payload_index(
|
|
collection_name=collection,
|
|
field_name=field,
|
|
field_schema="keyword",
|
|
)
|
|
accessor._indexes_ensured.add(collection)
|
|
|
|
|
|
async def _scroll_all(accessor: QdrantAccessor, collection: str,
|
|
filters: dict[str, str], limit: int) -> list[Any]:
|
|
client = await accessor.client()
|
|
if not filters:
|
|
return await _scroll_raw(client, collection, None, limit)
|
|
flt = _filter(filters)
|
|
try:
|
|
return await _scroll_raw(client, collection, flt, limit)
|
|
except UnexpectedResponse as exc:
|
|
if not _is_index_required(exc):
|
|
raise
|
|
await _ensure_indexes(client, accessor, collection)
|
|
return await _scroll_raw(client, collection, flt, limit)
|
|
|
|
|
|
async def list_tables(accessor: QdrantAccessor) -> list[str]:
|
|
client = await accessor.client()
|
|
result = await client.get_collections()
|
|
return sorted(item.name for item in result.collections)
|
|
|
|
|
|
async def table_exists(accessor: QdrantAccessor, name: str) -> bool:
|
|
client = await accessor.client()
|
|
return await client.collection_exists(name)
|
|
|
|
|
|
async def distinct_values(accessor: QdrantAccessor, table: str, column: str,
|
|
filters: dict[str, str], limit: int) -> list[str]:
|
|
points = await _scroll_all(accessor, table, filters, limit)
|
|
values = {
|
|
str(payload[column])
|
|
for point in points
|
|
if (payload := point.payload or {}).get(column) is not None
|
|
}
|
|
return sorted(values)
|
|
|
|
|
|
async def rows_matching(accessor: QdrantAccessor, table: str,
|
|
filters: dict[str, str], limit: int) -> list[dict]:
|
|
points = await _scroll_all(accessor, table, filters, limit)
|
|
return [_point_to_row(point, accessor.config.id_field) for point in points]
|
|
|
|
|
|
async def row_record(accessor: QdrantAccessor, table: str, id_field: str,
|
|
row_id: str) -> dict | None:
|
|
ids = _candidate_ids(row_id)
|
|
if not ids:
|
|
return None
|
|
client = await accessor.client()
|
|
found = await client.retrieve(collection_name=table,
|
|
ids=ids,
|
|
with_payload=True,
|
|
with_vectors=False)
|
|
if not found:
|
|
return None
|
|
return _point_to_row(found[0], id_field)
|
|
|
|
|
|
async def search_rows(accessor: QdrantAccessor, table: str, query_text: str,
|
|
limit: int) -> list[dict]:
|
|
key = (table, query_text, limit)
|
|
cached = accessor.cached_search(key)
|
|
if cached is not None:
|
|
return cached
|
|
client = await accessor.client()
|
|
response = await client.query_points(
|
|
collection_name=table,
|
|
query=models.Document(text=query_text,
|
|
model=accessor.config.embedding_model),
|
|
limit=limit,
|
|
with_payload=True,
|
|
)
|
|
rows: list[dict] = []
|
|
for point in response.points:
|
|
row = _point_to_row(point, accessor.config.id_field)
|
|
row["_score"] = point.score
|
|
rows.append(row)
|
|
accessor.store_search(key, rows)
|
|
return rows
|