Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:30:44 +08:00

225 lines
7.1 KiB
Python

import logging
from typing import Any
from mirage.cache.index import IndexCacheStore, IndexEntry
from mirage.core.dify._client import dify_post
from mirage.core.dify.path import resolve_path
from mirage.core.dify.read import segment_text
from mirage.core.dify.tree import normalize_slug
from mirage.core.dify.walk import walk
from mirage.types import PathSpec
from mirage.utils.key_prefix import mount_prefix_of, rekey
from mirage.utils.score import format_score
logger = logging.getLogger(__name__)
METHODS = {
"semantic": "semantic_search",
"fulltext": "full_text_search",
"hybrid": "hybrid_search",
"keyword": "keyword_search",
}
async def search_segments(
accessor,
query: str,
paths: list[PathSpec],
index: IndexCacheStore,
method: str = "semantic",
top_k: int = 10,
threshold: float = 0.0,
mount_prefix: str = "",
) -> bytes:
search_method = validate_args(query, method, top_k, threshold)
if not mount_prefix and paths:
mount_prefix = mount_prefix_of(paths[0].virtual,
paths[0].resource_path)
retrieval_model = {
"search_method": search_method,
"top_k": min(top_k, 100),
"score_threshold_enabled": threshold > 0,
"score_threshold": threshold,
"reranking_enable": False,
}
has_name_based_target = False
if paths:
conditions, has_name_based_target = await metadata_conditions(
accessor, paths, index)
if not conditions:
return b""
retrieval_model["metadata_filtering_conditions"] = {
"logical_operator": "or",
"conditions": conditions,
}
response = await dify_post(
accessor.config,
f"/datasets/{accessor.config.dataset_id}/retrieve",
{
"query": query,
"retrieval_model": retrieval_model
},
)
output = records_to_bytes(
response.get("records") or [], accessor.config.slug_metadata_name,
mount_prefix)
if paths and has_name_based_target and output == b"":
logger.debug(
"Dify scoped search returned no records for name-based documents; "
"check that Built-in Fields are enabled in Dify dataset metadata.")
return output
def validate_args(query: str, method: str, top_k: int,
threshold: float) -> str:
if not query:
raise ValueError("search: query is required")
if len(query) > 250:
raise ValueError("search: query cannot exceed 250 characters")
if top_k <= 0:
raise ValueError("search: top-k must be positive")
if threshold < 0 or threshold > 1:
raise ValueError("search: threshold must be in [0, 1]")
if method not in METHODS:
raise ValueError(
"search: method must be one of semantic, fulltext, hybrid, keyword"
)
return METHODS[method]
async def metadata_conditions(
accessor,
paths: list[PathSpec],
index: IndexCacheStore,
) -> tuple[list[dict], bool]:
targets = await target_entries(accessor, paths, index)
slug_values: list[str] = []
name_values: list[str] = []
for entry in targets.values():
if entry.extra.get("has_slug") is True:
slug_values.append(str(entry.extra["raw_slug"]))
else:
name_values.append(entry.name)
conditions: list[dict] = []
if slug_values:
conditions.append({
"name": accessor.config.slug_metadata_name,
"comparison_operator": "in",
"value": sorted(slug_values),
})
if name_values:
conditions.append({
"name": "document_name",
"comparison_operator": "in",
"value": sorted(name_values),
})
return conditions, bool(name_values)
async def target_entries(
accessor,
paths: list[PathSpec],
index: IndexCacheStore,
) -> dict[str, IndexEntry]:
targets: dict[str, IndexEntry] = {}
for path in paths:
resolved = await resolve_path(accessor, path, index)
if resolved.entry is not None and not resolved.is_dir:
targets[resolved.entry.id] = resolved.entry
continue
if resolved.is_dir:
children = await walk(accessor,
path,
index,
include_root=False,
strip_prefix=False)
for child in children:
child_spec = PathSpec.from_str_path(
child, rekey(path.virtual, path.resource_path, child))
child_resolved = await resolve_path(accessor, child_spec,
index)
if (child_resolved.entry is not None
and not child_resolved.is_dir):
targets[child_resolved.entry.id] = child_resolved.entry
return targets
def records_to_bytes(
records: list[dict[str, Any]],
slug_metadata_name: str,
mount_prefix: str,
) -> bytes:
contents: list[str] = []
for record in records:
segment = record.get("segment")
if not isinstance(segment, dict):
continue
header = format_record_header(record, slug_metadata_name, mount_prefix)
if header is None:
continue
content = segment_text(segment)
contents.append(f"{header}\n{content}")
if not contents:
return b""
return ("\n".join(contents) + "\n").encode()
def format_record_header(
record: dict[str, Any],
slug_metadata_name: str,
mount_prefix: str,
) -> str | None:
path = record_path(record, slug_metadata_name, mount_prefix)
if path is None:
return None
score = format_score(record.get("score"))
if score is None:
return path
return f"{path}:{score}"
def record_path(
record: dict[str, Any],
slug_metadata_name: str,
mount_prefix: str,
) -> str | None:
segment = record.get("segment")
if not isinstance(segment, dict):
return None
document = segment.get("document")
if not isinstance(document, dict):
return None
raw_path = document_path(document, slug_metadata_name)
if raw_path is None:
return None
try:
normalized = normalize_slug(raw_path)
except ValueError:
logger.debug("Skipping Dify record with invalid slug/name: %r",
raw_path)
return None
prefix = mount_prefix.rstrip("/")
if not prefix:
return normalized
return prefix + normalized
def document_path(
document: dict[str, Any],
slug_metadata_name: str,
) -> str | None:
metadata = document.get("doc_metadata")
if isinstance(metadata, list):
for item in metadata:
if (isinstance(item, dict)
and item.get("name") == slug_metadata_name
and item.get("value") is not None):
return str(item["value"])
if isinstance(metadata,
dict) and metadata.get(slug_metadata_name) is not None:
return str(metadata[slug_metadata_name])
name = document.get("name")
if name is None:
return None
return str(name)