smartpipe-cli 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- smartpipe/__init__.py +6 -0
- smartpipe/__main__.py +8 -0
- smartpipe/assets/probe.png +0 -0
- smartpipe/assets/probe.txt +1 -0
- smartpipe/assets/probe.wav +0 -0
- smartpipe/cli/__init__.py +5 -0
- smartpipe/cli/auth_cmd.py +78 -0
- smartpipe/cli/cache_cmd.py +60 -0
- smartpipe/cli/chart_cmd.py +60 -0
- smartpipe/cli/cite_cmd.py +26 -0
- smartpipe/cli/cluster_cmd.py +102 -0
- smartpipe/cli/completions.py +91 -0
- smartpipe/cli/config_cmd.py +234 -0
- smartpipe/cli/diff_cmd.py +100 -0
- smartpipe/cli/distinct_cmd.py +94 -0
- smartpipe/cli/doctor_cmd.py +207 -0
- smartpipe/cli/echo_cmd.py +44 -0
- smartpipe/cli/embed_cmd.py +80 -0
- smartpipe/cli/extend_cmd.py +138 -0
- smartpipe/cli/filter_cmd.py +87 -0
- smartpipe/cli/getschema_cmd.py +31 -0
- smartpipe/cli/input_options.py +113 -0
- smartpipe/cli/interrupts.py +92 -0
- smartpipe/cli/join_cmd.py +162 -0
- smartpipe/cli/map_cmd.py +150 -0
- smartpipe/cli/outliers_cmd.py +82 -0
- smartpipe/cli/probe_cmd.py +223 -0
- smartpipe/cli/reduce_cmd.py +129 -0
- smartpipe/cli/root.py +281 -0
- smartpipe/cli/run_cmd.py +136 -0
- smartpipe/cli/sample_cmd.py +35 -0
- smartpipe/cli/schema_cmd.py +75 -0
- smartpipe/cli/screens.py +231 -0
- smartpipe/cli/sem_file.py +453 -0
- smartpipe/cli/sort_cmd.py +33 -0
- smartpipe/cli/split_cmd.py +76 -0
- smartpipe/cli/summarize_cmd.py +37 -0
- smartpipe/cli/top_k_cmd.py +97 -0
- smartpipe/cli/usage_cmd.py +66 -0
- smartpipe/cli/where_cmd.py +36 -0
- smartpipe/config/__init__.py +5 -0
- smartpipe/config/credentials.py +118 -0
- smartpipe/config/display.py +70 -0
- smartpipe/config/doctor.py +58 -0
- smartpipe/config/paths.py +38 -0
- smartpipe/config/store.py +252 -0
- smartpipe/container.py +439 -0
- smartpipe/core/__init__.py +5 -0
- smartpipe/core/errors.py +57 -0
- smartpipe/core/jsontools.py +56 -0
- smartpipe/engine/__init__.py +9 -0
- smartpipe/engine/aggregate.py +234 -0
- smartpipe/engine/blocking.py +44 -0
- smartpipe/engine/chart.py +143 -0
- smartpipe/engine/chunking.py +161 -0
- smartpipe/engine/clustering.py +94 -0
- smartpipe/engine/predicate.py +330 -0
- smartpipe/engine/prompts.py +601 -0
- smartpipe/engine/ranking.py +62 -0
- smartpipe/engine/runner.py +175 -0
- smartpipe/engine/schema.py +208 -0
- smartpipe/engine/schema_dsl.py +144 -0
- smartpipe/engine/tally.py +53 -0
- smartpipe/engine/timebin.py +67 -0
- smartpipe/engine/units.py +43 -0
- smartpipe/engine/windows.py +78 -0
- smartpipe/io/__init__.py +9 -0
- smartpipe/io/diagnostics.py +148 -0
- smartpipe/io/inputs.py +44 -0
- smartpipe/io/items.py +149 -0
- smartpipe/io/leaderboard.py +52 -0
- smartpipe/io/metering.py +180 -0
- smartpipe/io/progress.py +140 -0
- smartpipe/io/readers.py +455 -0
- smartpipe/io/text.py +40 -0
- smartpipe/io/tty.py +88 -0
- smartpipe/io/usage.py +214 -0
- smartpipe/io/writers.py +340 -0
- smartpipe/models/__init__.py +5 -0
- smartpipe/models/anthropic_adapter.py +149 -0
- smartpipe/models/base.py +170 -0
- smartpipe/models/budget.py +94 -0
- smartpipe/models/cache.py +132 -0
- smartpipe/models/gemini_native.py +196 -0
- smartpipe/models/http_support.py +77 -0
- smartpipe/models/jina.py +98 -0
- smartpipe/models/local_embed.py +76 -0
- smartpipe/models/ollama.py +204 -0
- smartpipe/models/openai_codex.py +237 -0
- smartpipe/models/openai_compat.py +328 -0
- smartpipe/models/openai_oauth.py +366 -0
- smartpipe/models/resolve.py +78 -0
- smartpipe/models/retry.py +69 -0
- smartpipe/models/stt.py +80 -0
- smartpipe/models/windows.py +116 -0
- smartpipe/parsing/__init__.py +10 -0
- smartpipe/parsing/detect.py +178 -0
- smartpipe/parsing/extract.py +582 -0
- smartpipe/py.typed +0 -0
- smartpipe/verbs/__init__.py +5 -0
- smartpipe/verbs/chart.py +153 -0
- smartpipe/verbs/cluster.py +220 -0
- smartpipe/verbs/common.py +468 -0
- smartpipe/verbs/convert.py +251 -0
- smartpipe/verbs/diff.py +206 -0
- smartpipe/verbs/distinct.py +164 -0
- smartpipe/verbs/embed.py +166 -0
- smartpipe/verbs/extend.py +180 -0
- smartpipe/verbs/filter.py +191 -0
- smartpipe/verbs/getschema.py +135 -0
- smartpipe/verbs/join.py +413 -0
- smartpipe/verbs/map.py +315 -0
- smartpipe/verbs/outliers.py +119 -0
- smartpipe/verbs/reduce.py +428 -0
- smartpipe/verbs/sample.py +52 -0
- smartpipe/verbs/sortverb.py +63 -0
- smartpipe/verbs/split.py +333 -0
- smartpipe/verbs/summarize.py +60 -0
- smartpipe/verbs/top_k.py +318 -0
- smartpipe/verbs/where.py +47 -0
- smartpipe_cli-1.3.0.dist-info/METADATA +192 -0
- smartpipe_cli-1.3.0.dist-info/RECORD +126 -0
- smartpipe_cli-1.3.0.dist-info/WHEEL +4 -0
- smartpipe_cli-1.3.0.dist-info/entry_points.txt +3 -0
- smartpipe_cli-1.3.0.dist-info/licenses/LICENSE +201 -0
- smartpipe_cli-1.3.0.dist-info/licenses/NOTICE +5 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Dynamic context-window discovery (D26 layer 1).
|
|
2
|
+
|
|
3
|
+
Four of the six wires publish their window; asking costs one cheap metadata GET,
|
|
4
|
+
so the probe runs at most once per run, lazily, and only when the static table's
|
|
5
|
+
budget already looks too small. A failed probe is never fatal: the conservative
|
|
6
|
+
table stays the floor, and reduce's bisection is the backstop when everything lies.
|
|
7
|
+
OpenAI and Anthropic don't expose window size via API; ``SMARTPIPE_CONTEXT_TOKENS``
|
|
8
|
+
covers them (and overrides everything else).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from smartpipe.core.jsontools import as_items, as_record
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from collections.abc import Mapping
|
|
19
|
+
|
|
20
|
+
import httpx
|
|
21
|
+
|
|
22
|
+
from smartpipe.models.base import ModelRef
|
|
23
|
+
|
|
24
|
+
__all__ = ["probe_context_window"]
|
|
25
|
+
|
|
26
|
+
_PROBE_TIMEOUT_S = 5.0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def probe_context_window(
|
|
30
|
+
ref: ModelRef, *, client: httpx.AsyncClient, env: Mapping[str, str]
|
|
31
|
+
) -> int | None:
|
|
32
|
+
"""The provider's own answer to "how big is this model's window?", or None."""
|
|
33
|
+
try:
|
|
34
|
+
match ref.provider:
|
|
35
|
+
case "ollama":
|
|
36
|
+
return await _ollama(ref.name, client, env)
|
|
37
|
+
case "mistral":
|
|
38
|
+
return await _mistral(ref.name, client, env)
|
|
39
|
+
case "openrouter":
|
|
40
|
+
return await _openrouter(ref.name, client, env)
|
|
41
|
+
case "gemini":
|
|
42
|
+
return await _gemini(ref.name, client, env)
|
|
43
|
+
case _: # openai/anthropic publish no window via API
|
|
44
|
+
return None
|
|
45
|
+
except Exception:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def _ollama(name: str, client: httpx.AsyncClient, env: Mapping[str, str]) -> int | None:
|
|
50
|
+
from smartpipe.models.ollama import resolve_host
|
|
51
|
+
|
|
52
|
+
response = await client.post(
|
|
53
|
+
f"{resolve_host(env)}/api/show", json={"model": name}, timeout=_PROBE_TIMEOUT_S
|
|
54
|
+
)
|
|
55
|
+
response.raise_for_status()
|
|
56
|
+
record = as_record(response.json())
|
|
57
|
+
info = as_record(record.get("model_info")) if record is not None else None
|
|
58
|
+
if info is None:
|
|
59
|
+
return None
|
|
60
|
+
# the key is architecture-prefixed: "llama.context_length", "qwen3.context_length", …
|
|
61
|
+
lengths = (value for key, value in info.items() if key.endswith(".context_length"))
|
|
62
|
+
first = next(lengths, None)
|
|
63
|
+
return first if isinstance(first, int) else None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def _mistral(name: str, client: httpx.AsyncClient, env: Mapping[str, str]) -> int | None:
|
|
67
|
+
from smartpipe.models.openai_compat import MISTRAL_WIRE, resolve_base_url
|
|
68
|
+
|
|
69
|
+
key = env.get(MISTRAL_WIRE.key_env, "").strip()
|
|
70
|
+
if not key:
|
|
71
|
+
return None
|
|
72
|
+
response = await client.get(
|
|
73
|
+
f"{resolve_base_url(env, MISTRAL_WIRE)}/v1/models/{name}",
|
|
74
|
+
headers={"Authorization": f"Bearer {key}"},
|
|
75
|
+
timeout=_PROBE_TIMEOUT_S,
|
|
76
|
+
)
|
|
77
|
+
response.raise_for_status()
|
|
78
|
+
record = as_record(response.json())
|
|
79
|
+
value = record.get("max_context_length") if record is not None else None
|
|
80
|
+
return value if isinstance(value, int) else None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def _openrouter(name: str, client: httpx.AsyncClient, env: Mapping[str, str]) -> int | None:
|
|
84
|
+
from smartpipe.models.openai_compat import OPENROUTER_WIRE, resolve_base_url
|
|
85
|
+
|
|
86
|
+
response = await client.get(
|
|
87
|
+
f"{resolve_base_url(env, OPENROUTER_WIRE)}/v1/models", timeout=_PROBE_TIMEOUT_S
|
|
88
|
+
)
|
|
89
|
+
response.raise_for_status()
|
|
90
|
+
record = as_record(response.json())
|
|
91
|
+
rows = as_items(record.get("data")) if record is not None else None
|
|
92
|
+
if rows is None:
|
|
93
|
+
return None
|
|
94
|
+
for row in rows:
|
|
95
|
+
entry = as_record(row)
|
|
96
|
+
if entry is not None and entry.get("id") == name:
|
|
97
|
+
value = entry.get("context_length")
|
|
98
|
+
return value if isinstance(value, int) else None
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def _gemini(name: str, client: httpx.AsyncClient, env: Mapping[str, str]) -> int | None:
|
|
103
|
+
from smartpipe.models.openai_compat import GEMINI_WIRE, resolve_base_url
|
|
104
|
+
|
|
105
|
+
key = env.get(GEMINI_WIRE.key_env, "").strip()
|
|
106
|
+
if not key:
|
|
107
|
+
return None
|
|
108
|
+
# the native endpoint lives one path segment above the OpenAI-compat root
|
|
109
|
+
native = resolve_base_url(env, GEMINI_WIRE).removesuffix("/openai")
|
|
110
|
+
response = await client.get(
|
|
111
|
+
f"{native}/models/{name}", headers={"x-goog-api-key": key}, timeout=_PROBE_TIMEOUT_S
|
|
112
|
+
)
|
|
113
|
+
response.raise_for_status()
|
|
114
|
+
record = as_record(response.json())
|
|
115
|
+
value = record.get("inputTokenLimit") if record is not None else None
|
|
116
|
+
return value if isinstance(value, int) else None
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""File parsing: detect a file's kind, extract its text (spec §3.1, D08).
|
|
2
|
+
|
|
3
|
+
The user never names a parser. ``detect`` sniffs the kind (pure, never raises);
|
|
4
|
+
``extract`` turns it into text, lazy-importing the optional markitdown bridge only
|
|
5
|
+
when a document actually needs it.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Detect a file's kind (spec §3.1, stage-07 contract) — pure, never raises.
|
|
2
|
+
|
|
3
|
+
Extension first, magic bytes as a backstop. Detection only *classifies*; extraction
|
|
4
|
+
(and any missing-dependency handling) lives in ``extract``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import TYPE_CHECKING, Literal
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
__all__ = ["FileKind", "Route", "audio_mime", "detect_kind", "route", "video_mime"]
|
|
16
|
+
|
|
17
|
+
Route = Literal["text", "doc", "audio", "video", "image", "skip"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FileKind(Enum):
|
|
21
|
+
TEXT = "text"
|
|
22
|
+
MARKDOWN = "markdown"
|
|
23
|
+
CSV = "csv"
|
|
24
|
+
JSON = "json"
|
|
25
|
+
PDF = "pdf"
|
|
26
|
+
DOCX = "docx"
|
|
27
|
+
XLSX = "xlsx"
|
|
28
|
+
PPTX = "pptx"
|
|
29
|
+
HTML = "html"
|
|
30
|
+
EPUB = "epub"
|
|
31
|
+
AUDIO = "audio"
|
|
32
|
+
VIDEO = "video"
|
|
33
|
+
IMAGE = "image"
|
|
34
|
+
UNKNOWN_BINARY = "unknown-binary"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
_BY_EXTENSION: dict[str, FileKind] = {
|
|
38
|
+
".txt": FileKind.TEXT,
|
|
39
|
+
".text": FileKind.TEXT,
|
|
40
|
+
".log": FileKind.TEXT,
|
|
41
|
+
".md": FileKind.MARKDOWN,
|
|
42
|
+
".markdown": FileKind.MARKDOWN,
|
|
43
|
+
".csv": FileKind.CSV,
|
|
44
|
+
".tsv": FileKind.CSV,
|
|
45
|
+
".json": FileKind.JSON,
|
|
46
|
+
".jsonl": FileKind.JSON,
|
|
47
|
+
".ndjson": FileKind.JSON,
|
|
48
|
+
".pdf": FileKind.PDF,
|
|
49
|
+
".docx": FileKind.DOCX,
|
|
50
|
+
".xlsx": FileKind.XLSX,
|
|
51
|
+
".pptx": FileKind.PPTX,
|
|
52
|
+
".html": FileKind.HTML,
|
|
53
|
+
".htm": FileKind.HTML,
|
|
54
|
+
".epub": FileKind.EPUB,
|
|
55
|
+
".mp3": FileKind.AUDIO,
|
|
56
|
+
".wav": FileKind.AUDIO,
|
|
57
|
+
".flac": FileKind.AUDIO,
|
|
58
|
+
".m4a": FileKind.AUDIO,
|
|
59
|
+
".ogg": FileKind.AUDIO,
|
|
60
|
+
".mp4": FileKind.VIDEO,
|
|
61
|
+
".mov": FileKind.VIDEO,
|
|
62
|
+
".mkv": FileKind.VIDEO,
|
|
63
|
+
".webm": FileKind.VIDEO,
|
|
64
|
+
".png": FileKind.IMAGE,
|
|
65
|
+
".jpg": FileKind.IMAGE,
|
|
66
|
+
".jpeg": FileKind.IMAGE,
|
|
67
|
+
".gif": FileKind.IMAGE,
|
|
68
|
+
".webp": FileKind.IMAGE,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
_ROUTES: dict[FileKind, Route] = {
|
|
72
|
+
FileKind.TEXT: "text",
|
|
73
|
+
FileKind.MARKDOWN: "text",
|
|
74
|
+
FileKind.CSV: "text",
|
|
75
|
+
FileKind.JSON: "text",
|
|
76
|
+
FileKind.PDF: "doc",
|
|
77
|
+
FileKind.DOCX: "doc",
|
|
78
|
+
FileKind.XLSX: "doc",
|
|
79
|
+
FileKind.PPTX: "doc",
|
|
80
|
+
FileKind.HTML: "doc",
|
|
81
|
+
FileKind.EPUB: "doc",
|
|
82
|
+
FileKind.AUDIO: "audio",
|
|
83
|
+
FileKind.VIDEO: "video",
|
|
84
|
+
FileKind.IMAGE: "image",
|
|
85
|
+
FileKind.UNKNOWN_BINARY: "skip",
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
_AUDIO_MIME_BY_SUFFIX = {
|
|
90
|
+
".mp3": "audio/mpeg",
|
|
91
|
+
".wav": "audio/wav",
|
|
92
|
+
".flac": "audio/flac",
|
|
93
|
+
".m4a": "audio/mp4",
|
|
94
|
+
".ogg": "audio/ogg",
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def audio_mime(path: Path) -> str:
|
|
99
|
+
"""The wire mime for an audio file — suffix-driven, mp3 as the safe default."""
|
|
100
|
+
return _AUDIO_MIME_BY_SUFFIX.get(path.suffix.lower(), "audio/mpeg")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def route(kind: FileKind) -> Route:
|
|
104
|
+
return _ROUTES[kind]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def detect_kind(path: Path, head: bytes) -> FileKind:
|
|
108
|
+
by_ext = _BY_EXTENSION.get(path.suffix.lower())
|
|
109
|
+
if by_ext is not None:
|
|
110
|
+
return by_ext
|
|
111
|
+
return _sniff(head)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _sniff(head: bytes) -> FileKind:
|
|
115
|
+
if head.startswith(b"%PDF"):
|
|
116
|
+
return FileKind.PDF
|
|
117
|
+
if head.startswith(b"PK\x03\x04"):
|
|
118
|
+
return _sniff_zip(head)
|
|
119
|
+
if _is_audio(head):
|
|
120
|
+
return FileKind.AUDIO
|
|
121
|
+
if len(head) >= 12 and head[4:8] == b"ftyp":
|
|
122
|
+
return FileKind.VIDEO # mp4/mov family
|
|
123
|
+
if _is_image(head):
|
|
124
|
+
return FileKind.IMAGE
|
|
125
|
+
if _is_utf8(head):
|
|
126
|
+
return FileKind.TEXT
|
|
127
|
+
return FileKind.UNKNOWN_BINARY
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _sniff_zip(head: bytes) -> FileKind:
|
|
131
|
+
# OOXML/EPUB are zips; the inner directory names appear near the front
|
|
132
|
+
if b"word/" in head:
|
|
133
|
+
return FileKind.DOCX
|
|
134
|
+
if b"xl/" in head:
|
|
135
|
+
return FileKind.XLSX
|
|
136
|
+
if b"ppt/" in head:
|
|
137
|
+
return FileKind.PPTX
|
|
138
|
+
if b"epub" in head or b"mimetype" in head:
|
|
139
|
+
return FileKind.EPUB
|
|
140
|
+
return FileKind.UNKNOWN_BINARY
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _is_audio(head: bytes) -> bool:
|
|
144
|
+
if head.startswith((b"ID3", b"fLaC", b"OggS")):
|
|
145
|
+
return True
|
|
146
|
+
if head.startswith(b"\xff\xfb") or head.startswith(b"\xff\xf3"): # MP3 frame sync
|
|
147
|
+
return True
|
|
148
|
+
return head.startswith(b"RIFF") and head[8:12] == b"WAVE"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _is_image(head: bytes) -> bool:
|
|
152
|
+
if head.startswith((b"\x89PNG\r\n\x1a\n", b"\xff\xd8\xff", b"GIF87a", b"GIF89a")):
|
|
153
|
+
return True
|
|
154
|
+
return head.startswith(b"RIFF") and head[8:12] == b"WEBP"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _is_utf8(head: bytes) -> bool:
|
|
158
|
+
if b"\x00" in head:
|
|
159
|
+
return False # NUL bytes never appear in text; a strong binary signal
|
|
160
|
+
try:
|
|
161
|
+
head.decode("utf-8")
|
|
162
|
+
except UnicodeDecodeError as exc:
|
|
163
|
+
# tolerate an invalid byte only in the last 3 positions — a multibyte char
|
|
164
|
+
# truncated at the 8 KiB sniff boundary, not mid-stream binary
|
|
165
|
+
return exc.start >= len(head) - 3
|
|
166
|
+
return True
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
_VIDEO_MIME_BY_SUFFIX = {
|
|
170
|
+
".mp4": "video/mp4",
|
|
171
|
+
".mov": "video/quicktime",
|
|
172
|
+
".mkv": "video/x-matroska",
|
|
173
|
+
".webm": "video/webm",
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def video_mime(path: Path) -> str:
|
|
178
|
+
return _VIDEO_MIME_BY_SUFFIX.get(path.suffix.lower(), "video/mp4")
|