pi-durable-python 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.
- pi_durable/__init__.py +320 -0
- pi_durable/context.py +328 -0
- pi_durable/delta.py +269 -0
- pi_durable/documents.py +366 -0
- pi_durable/entries.py +66 -0
- pi_durable/env/__init__.py +408 -0
- pi_durable/env/decode.py +48 -0
- pi_durable/env/line_scan.py +190 -0
- pi_durable/env/posix.py +1547 -0
- pi_durable/errors.py +89 -0
- pi_durable/harness/__init__.py +1 -0
- pi_durable/harness/agent.py +344 -0
- pi_durable/harness/compaction.py +673 -0
- pi_durable/harness/context.py +515 -0
- pi_durable/harness/define.py +111 -0
- pi_durable/harness/events.py +766 -0
- pi_durable/harness/generation.py +1079 -0
- pi_durable/harness/harness.py +570 -0
- pi_durable/harness/inbox.py +255 -0
- pi_durable/harness/live.py +374 -0
- pi_durable/harness/output.py +467 -0
- pi_durable/harness/prompt.py +288 -0
- pi_durable/harness/provider.py +78 -0
- pi_durable/harness/registry.py +136 -0
- pi_durable/harness/scheduler.py +1977 -0
- pi_durable/harness/submissions.py +357 -0
- pi_durable/harness/task_graph.py +334 -0
- pi_durable/harness/tool.py +1365 -0
- pi_durable/harness/types.py +772 -0
- pi_durable/harness/usage.py +88 -0
- pi_durable/harness/util.py +204 -0
- pi_durable/harness/validation.py +460 -0
- pi_durable/harness/view.py +324 -0
- pi_durable/ids.py +63 -0
- pi_durable/json.py +186 -0
- pi_durable/providers/__init__.py +362 -0
- pi_durable/providers/anthropic.py +603 -0
- pi_durable/providers/base.py +97 -0
- pi_durable/providers/json_parse.py +117 -0
- pi_durable/providers/openai.py +574 -0
- pi_durable/providers/request.py +35 -0
- pi_durable/providers/retry.py +127 -0
- pi_durable/providers/stream.py +183 -0
- pi_durable/providers/transcript.py +224 -0
- pi_durable/providers/usage.py +58 -0
- pi_durable/py.typed +0 -0
- pi_durable/search.py +115 -0
- pi_durable/session/__init__.py +5 -0
- pi_durable/session/drafts.py +762 -0
- pi_durable/session/forks.py +109 -0
- pi_durable/session/observation.py +374 -0
- pi_durable/session/session.py +1193 -0
- pi_durable/session/transaction.py +1088 -0
- pi_durable/storage/__init__.py +5 -0
- pi_durable/storage/jsonl/__init__.py +15 -0
- pi_durable/storage/jsonl/storage.py +958 -0
- pi_durable/storage/memory.py +889 -0
- pi_durable/storage/scan.py +57 -0
- pi_durable/storage/sqlite/__init__.py +26 -0
- pi_durable/storage/sqlite/database.py +62 -0
- pi_durable/storage/sqlite/migrations.py +191 -0
- pi_durable/storage/sqlite/posix.py +314 -0
- pi_durable/storage/sqlite/schema.py +97 -0
- pi_durable/storage/sqlite/storage.py +1130 -0
- pi_durable/tasks.py +89 -0
- pi_durable/testing/__init__.py +41 -0
- pi_durable/testing/assertions.py +44 -0
- pi_durable/testing/env_conformance.py +828 -0
- pi_durable/testing/runner.py +64 -0
- pi_durable/testing/storage_conformance.py +2008 -0
- pi_durable/testing/types.py +99 -0
- pi_durable/tool_search/__init__.py +159 -0
- pi_durable/tools/__init__.py +81 -0
- pi_durable/tools/_values.py +43 -0
- pi_durable/tools/bash.py +250 -0
- pi_durable/tools/edit.py +214 -0
- pi_durable/tools/edit_diff.py +452 -0
- pi_durable/tools/env.py +19 -0
- pi_durable/tools/file_mutation_queue.py +68 -0
- pi_durable/tools/image.py +248 -0
- pi_durable/tools/image_processor.py +77 -0
- pi_durable/tools/path_utils.py +111 -0
- pi_durable/tools/read.py +506 -0
- pi_durable/tools/write.py +71 -0
- pi_durable/truncate.py +185 -0
- pi_durable/types.py +1297 -0
- pi_durable_python-1.3.0.dist-info/METADATA +276 -0
- pi_durable_python-1.3.0.dist-info/RECORD +90 -0
- pi_durable_python-1.3.0.dist-info/WHEEL +4 -0
- pi_durable_python-1.3.0.dist-info/licenses/LICENSE +21 -0
pi_durable/search.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""BM25 tool search for the ``tool_search`` tool.
|
|
2
|
+
|
|
3
|
+
Port of the v1.3.0 ``src/search.ts``, the ranking the coding agent uses and durable keeps its own
|
|
4
|
+
copy of. Terms are lowercased, split at camel-case boundaries and non-alphanumerics, stripped of
|
|
5
|
+
stop words, and stemmed with the same naive singular rule. Okapi BM25 (``k1`` 1.2, ``b`` 0.75)
|
|
6
|
+
orders documents by score; equal scores keep document order.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
import re
|
|
13
|
+
from collections.abc import Sequence
|
|
14
|
+
from typing import TypedDict
|
|
15
|
+
|
|
16
|
+
K1 = 1.2
|
|
17
|
+
B = 0.75
|
|
18
|
+
|
|
19
|
+
STOP_WORDS = frozenset(
|
|
20
|
+
{
|
|
21
|
+
"a",
|
|
22
|
+
"an",
|
|
23
|
+
"and",
|
|
24
|
+
"are",
|
|
25
|
+
"as",
|
|
26
|
+
"at",
|
|
27
|
+
"be",
|
|
28
|
+
"by",
|
|
29
|
+
"for",
|
|
30
|
+
"from",
|
|
31
|
+
"in",
|
|
32
|
+
"is",
|
|
33
|
+
"it",
|
|
34
|
+
"of",
|
|
35
|
+
"on",
|
|
36
|
+
"or",
|
|
37
|
+
"that",
|
|
38
|
+
"the",
|
|
39
|
+
"this",
|
|
40
|
+
"to",
|
|
41
|
+
"with",
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
_CAMEL_BOUNDARY = re.compile(r"([a-z0-9])([A-Z])")
|
|
46
|
+
_ACRONYM_BOUNDARY = re.compile(r"([A-Z]+)([A-Z][a-z])")
|
|
47
|
+
_PLURAL_ES = re.compile(r"(ches|shes|sses|xes|zes)$")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SearchDocument(TypedDict):
|
|
51
|
+
"""One searchable tool: its name and all text the ranker reads."""
|
|
52
|
+
|
|
53
|
+
name: str
|
|
54
|
+
text: str
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _stem(term: str) -> str:
|
|
58
|
+
"""Naive singular form, so ``issues`` matches ``issue`` and ``searches`` matches ``search``."""
|
|
59
|
+
|
|
60
|
+
if len(term) > 4 and term.endswith("ies"):
|
|
61
|
+
return f"{term[:-3]}y"
|
|
62
|
+
if len(term) > 4 and _PLURAL_ES.search(term):
|
|
63
|
+
return term[:-2]
|
|
64
|
+
if len(term) > 3 and term.endswith("s") and not term.endswith("ss"):
|
|
65
|
+
return term[:-1]
|
|
66
|
+
return term
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def tokenize(text: str) -> list[str]:
|
|
70
|
+
"""Lowercase terms, split at camelCase boundaries and non-alphanumerics, without stop words."""
|
|
71
|
+
|
|
72
|
+
separated = _CAMEL_BOUNDARY.sub(r"\1 \2", text)
|
|
73
|
+
separated = _ACRONYM_BOUNDARY.sub(r"\1 \2", separated)
|
|
74
|
+
return [
|
|
75
|
+
_stem(term) for term in re.split(r"[^a-z0-9]+", separated.lower()) if term and term not in STOP_WORDS
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def rank_documents(query: str, documents: Sequence[SearchDocument], limit: int) -> list[str]:
|
|
80
|
+
"""Okapi BM25 names of the best matches, best first; ties keep document order."""
|
|
81
|
+
|
|
82
|
+
query_terms = list(dict.fromkeys(tokenize(query)))
|
|
83
|
+
if not query_terms or not documents or limit <= 0:
|
|
84
|
+
return []
|
|
85
|
+
term_counts = [_counts(tokenize(document["text"])) for document in documents]
|
|
86
|
+
lengths = [sum(counts.values()) for counts in term_counts]
|
|
87
|
+
total_length = sum(lengths)
|
|
88
|
+
average_length = total_length / len(documents) if total_length else 1.0
|
|
89
|
+
idf: dict[str, float] = {}
|
|
90
|
+
for term in query_terms:
|
|
91
|
+
frequency = sum(1 for counts in term_counts if term in counts)
|
|
92
|
+
idf[term] = math.log(1 + (len(documents) - frequency + 0.5) / (frequency + 0.5))
|
|
93
|
+
matches: list[tuple[str, float]] = []
|
|
94
|
+
for index, document in enumerate(documents):
|
|
95
|
+
score = 0.0
|
|
96
|
+
norm = K1 * (1 - B + (B * lengths[index]) / average_length)
|
|
97
|
+
for term in query_terms:
|
|
98
|
+
count = term_counts[index].get(term)
|
|
99
|
+
if not count:
|
|
100
|
+
continue
|
|
101
|
+
score += idf[term] * ((count * (K1 + 1)) / (count + norm))
|
|
102
|
+
if score > 0:
|
|
103
|
+
matches.append((document["name"], score))
|
|
104
|
+
matches.sort(key=lambda match: -match[1])
|
|
105
|
+
return [name for name, _score in matches[:limit]]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _counts(terms: Sequence[str]) -> dict[str, int]:
|
|
109
|
+
counts: dict[str, int] = {}
|
|
110
|
+
for term in terms:
|
|
111
|
+
counts[term] = counts.get(term, 0) + 1
|
|
112
|
+
return counts
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
__all__ = ["SearchDocument", "rank_documents", "tokenize"]
|