pal-code 0.1.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.
- pal/__init__.py +29 -0
- pal/__main__.py +3 -0
- pal/access_manager/__init__.py +16 -0
- pal/access_manager/components/__init__.py +14 -0
- pal/access_manager/components/access_control.py +41 -0
- pal/access_manager/components/base.py +39 -0
- pal/access_manager/components/intervention.py +31 -0
- pal/access_manager/components/privilege_registry.py +30 -0
- pal/access_manager/pipeline.py +68 -0
- pal/access_manager/utils/__init__.py +2 -0
- pal/agent_constraints/__init__.py +3 -0
- pal/agent_constraints/components/AGENT_OUTPUT.md +13 -0
- pal/agent_constraints/components/__init__.py +3 -0
- pal/agent_constraints/components/constraints.py +22 -0
- pal/agent_constraints/pipeline.py +22 -0
- pal/agent_constraints/utils/__init__.py +2 -0
- pal/agents/__init__.py +12 -0
- pal/agents/coding.py +14 -0
- pal/agents/docs.py +15 -0
- pal/chat/__init__.py +4 -0
- pal/chat/demo.py +231 -0
- pal/chat/providers.py +23 -0
- pal/chat/service.py +591 -0
- pal/chat/services/__init__.py +26 -0
- pal/chat/services/base.py +142 -0
- pal/chat/services/data.py +142 -0
- pal/chat/services/media.py +123 -0
- pal/chat/services/web.py +60 -0
- pal/chat/sessions.py +108 -0
- pal/chat/tools.py +446 -0
- pal/chat/uploads.py +81 -0
- pal/cli.py +502 -0
- pal/compression/__init__.py +12 -0
- pal/compression/components/__init__.py +64 -0
- pal/compression/components/adverbs.py +20 -0
- pal/compression/components/hedges.py +54 -0
- pal/compression/components/interjections.py +20 -0
- pal/compression/components/minifier.py +56 -0
- pal/compression/components/output_compressor.py +13 -0
- pal/compression/components/pleonasms.py +54 -0
- pal/compression/components/punctuations.py +36 -0
- pal/compression/components/stopwords.py +62 -0
- pal/compression/history.py +346 -0
- pal/compression/pipeline.py +58 -0
- pal/compression/utils/__init__.py +30 -0
- pal/compression/utils/negations.py +18 -0
- pal/compression/utils/nltk_setup.py +20 -0
- pal/compression/utils/pronouns.py +27 -0
- pal/compression/utils/proselint_setup.py +9 -0
- pal/compression/utils/spacy_setup.py +22 -0
- pal/config/default.yaml +163 -0
- pal/config.py +504 -0
- pal/context_manager/__init__.py +7 -0
- pal/context_manager/chat_context.py +117 -0
- pal/context_manager/components/__init__.py +5 -0
- pal/context_manager/components/assembler.py +75 -0
- pal/context_manager/components/context_cache.py +45 -0
- pal/context_manager/components/kv_bridge.py +16 -0
- pal/context_manager/components/pager.py +48 -0
- pal/context_manager/components/simple_context_manager.py +56 -0
- pal/context_manager/components/turns.py +88 -0
- pal/context_manager/pipeline.py +27 -0
- pal/context_manager/utils/__init__.py +18 -0
- pal/context_manager/utils/hashing.py +5 -0
- pal/context_manager/utils/relevance.py +59 -0
- pal/context_manager/utils/storage.py +37 -0
- pal/documents/__init__.py +4 -0
- pal/documents/components/__init__.py +0 -0
- pal/documents/components/chunker.py +79 -0
- pal/documents/components/index.py +155 -0
- pal/documents/utils/__init__.py +1 -0
- pal/errors.py +40 -0
- pal/input_converters/__init__.py +8 -0
- pal/input_converters/components/__init__.py +0 -0
- pal/input_converters/components/base.py +38 -0
- pal/input_converters/components/pandoc_converter.py +87 -0
- pal/input_converters/components/pdf_converter.py +46 -0
- pal/input_converters/components/pptx_converter.py +52 -0
- pal/input_converters/components/xlsx_converter.py +38 -0
- pal/input_converters/pipeline.py +72 -0
- pal/input_converters/utils/__init__.py +1 -0
- pal/input_converters/workspace.py +56 -0
- pal/input_savers/INPUT_RULES.md +2 -0
- pal/input_savers/__init__.py +5 -0
- pal/input_savers/components/__init__.py +0 -0
- pal/input_savers/components/base.py +40 -0
- pal/input_savers/components/methods.py +41 -0
- pal/input_savers/components/verifier.py +39 -0
- pal/input_savers/pipeline.py +154 -0
- pal/input_savers/utils/__init__.py +0 -0
- pal/input_savers/utils/guards.py +94 -0
- pal/knowledge_graph/__init__.py +10 -0
- pal/knowledge_graph/components/__init__.py +0 -0
- pal/knowledge_graph/components/base.py +50 -0
- pal/knowledge_graph/components/builder.py +54 -0
- pal/knowledge_graph/components/retriever.py +100 -0
- pal/knowledge_graph/components/workspace.py +116 -0
- pal/knowledge_graph/pipeline.py +215 -0
- pal/knowledge_graph/utils/__init__.py +1 -0
- pal/latency_tracker/__init__.py +3 -0
- pal/latency_tracker/pipeline.py +86 -0
- pal/llm_scheduler/__init__.py +11 -0
- pal/llm_scheduler/api.py +137 -0
- pal/llm_scheduler/code.py +28 -0
- pal/llm_scheduler/core.py +92 -0
- pal/llm_scheduler/local.py +215 -0
- pal/llm_scheduler/pipeline.py +42 -0
- pal/llm_scheduler/types.py +92 -0
- pal/llm_wrapper/__init__.py +34 -0
- pal/llm_wrapper/adapter.py +316 -0
- pal/llm_wrapper/components/__init__.py +23 -0
- pal/llm_wrapper/components/adapter_types.py +90 -0
- pal/llm_wrapper/components/base.py +195 -0
- pal/llm_wrapper/components/features.py +104 -0
- pal/llm_wrapper/components/litellm_wrapper.py +215 -0
- pal/llm_wrapper/components/openrouter_wrapper.py +114 -0
- pal/llm_wrapper/modes.py +73 -0
- pal/llm_wrapper/pipeline.py +61 -0
- pal/llm_wrapper/utils/__init__.py +4 -0
- pal/llm_wrapper/utils/registry.py +49 -0
- pal/llm_wrapper/utils/token_counting.py +10 -0
- pal/local_llm/__init__.py +5 -0
- pal/local_llm/components/__init__.py +0 -0
- pal/local_llm/components/huggingface_local.py +217 -0
- pal/local_llm/components/local_llm.py +60 -0
- pal/logging_setup.py +18 -0
- pal/memory/__init__.py +7 -0
- pal/memory/components/__init__.py +6 -0
- pal/memory/components/lru_k.py +56 -0
- pal/memory/components/manager.py +98 -0
- pal/memory/components/note.py +54 -0
- pal/memory/components/storage_manager.py +60 -0
- pal/memory/pipeline.py +122 -0
- pal/memory/utils/__init__.py +3 -0
- pal/memory/utils/keyword_overlap.py +3 -0
- pal/memory_tracker/__init__.py +3 -0
- pal/memory_tracker/components/__init__.py +0 -0
- pal/memory_tracker/components/process.py +56 -0
- pal/memory_tracker/pipeline.py +44 -0
- pal/metrics/__init__.py +4 -0
- pal/metrics/components/__init__.py +4 -0
- pal/metrics/components/collector.py +140 -0
- pal/metrics/components/exporter.py +28 -0
- pal/metrics/pipeline.py +49 -0
- pal/metrics/utils/__init__.py +2 -0
- pal/notes_manager/__init__.py +5 -0
- pal/notes_manager/components/__init__.py +0 -0
- pal/notes_manager/components/privacy.py +14 -0
- pal/notes_manager/components/recall.py +31 -0
- pal/notes_manager/components/tidy.py +28 -0
- pal/notes_manager/pipeline.py +138 -0
- pal/orchestrator/__init__.py +26 -0
- pal/orchestrator/components/__init__.py +26 -0
- pal/orchestrator/components/agent.py +105 -0
- pal/orchestrator/components/cgroup_manager.py +209 -0
- pal/orchestrator/components/context_interrupt.py +220 -0
- pal/orchestrator/components/limits.py +9 -0
- pal/orchestrator/components/scheduler.py +69 -0
- pal/orchestrator/components/syscall_scheduler.py +200 -0
- pal/orchestrator/pipeline.py +228 -0
- pal/orchestrator/utils/__init__.py +4 -0
- pal/orchestrator/utils/device.py +164 -0
- pal/orchestrator/utils/protocol.py +32 -0
- pal/os_kernel/__init__.py +5 -0
- pal/os_kernel/filesystem.py +80 -0
- pal/os_kernel/network.py +146 -0
- pal/os_kernel/pipeline.py +175 -0
- pal/os_kernel/sysinfo.py +36 -0
- pal/paths.py +59 -0
- pal/pipeline.py +177 -0
- pal/py.typed +0 -0
- pal/redact.py +19 -0
- pal/sdk/__init__.py +33 -0
- pal/sdk/adapters.py +43 -0
- pal/sdk/agent.py +99 -0
- pal/sdk/components/__init__.py +14 -0
- pal/sdk/components/autogen_adapter.py +65 -0
- pal/sdk/components/autogen_client.py +98 -0
- pal/sdk/components/framework_adapter_base.py +69 -0
- pal/sdk/components/metagpt_adapter.py +46 -0
- pal/sdk/components/open_interpreter_adapter.py +47 -0
- pal/sdk/components/tool_catalog.py +44 -0
- pal/sdk/os_api.py +84 -0
- pal/sdk/patterns.py +129 -0
- pal/sdk/pipeline.py +240 -0
- pal/sdk/queries.py +68 -0
- pal/sdk/savers.py +88 -0
- pal/sdk/utils/__init__.py +2 -0
- pal/storage_manager/__init__.py +17 -0
- pal/storage_manager/components/__init__.py +19 -0
- pal/storage_manager/components/base.py +50 -0
- pal/storage_manager/components/local_file_backend.py +84 -0
- pal/storage_manager/components/storage_queue.py +91 -0
- pal/storage_manager/components/vector_store_backend.py +122 -0
- pal/storage_manager/pipeline.py +153 -0
- pal/storage_manager/utils/__init__.py +4 -0
- pal/storage_manager/utils/memory_adapter.py +60 -0
- pal/storage_manager/utils/sdk.py +93 -0
- pal/studio/__init__.py +0 -0
- pal/studio/builder.py +166 -0
- pal/studio/catalog.py +90 -0
- pal/studio/checks.py +129 -0
- pal/studio/clients.py +6 -0
- pal/studio/editor.py +137 -0
- pal/studio/files.py +6 -0
- pal/studio/models.py +6 -0
- pal/studio/plan.py +149 -0
- pal/studio/planner.py +205 -0
- pal/studio/policy.py +69 -0
- pal/studio/routes.py +254 -0
- pal/studio/runs.py +210 -0
- pal/task_router/__init__.py +11 -0
- pal/task_router/components/__init__.py +0 -0
- pal/task_router/components/answerer.py +37 -0
- pal/task_router/components/base.py +34 -0
- pal/task_router/components/classifier.py +40 -0
- pal/task_router/components/gates.py +23 -0
- pal/task_router/pipeline.py +61 -0
- pal/task_router/utils/__init__.py +1 -0
- pal/terminal/__init__.py +1 -0
- pal/terminal/backend.py +141 -0
- pal/terminal/bench.py +173 -0
- pal/terminal/cli.py +826 -0
- pal/terminal/clients.py +170 -0
- pal/terminal/commands.py +401 -0
- pal/terminal/decide.py +242 -0
- pal/terminal/files.py +55 -0
- pal/terminal/flow_cli.py +439 -0
- pal/terminal/hardware.py +199 -0
- pal/terminal/history.py +88 -0
- pal/terminal/keys.py +83 -0
- pal/terminal/local_models.py +261 -0
- pal/terminal/logo.py +117 -0
- pal/terminal/models.py +99 -0
- pal/terminal/policy.py +54 -0
- pal/terminal/project.py +413 -0
- pal/terminal/review.py +65 -0
- pal/terminal/security.py +239 -0
- pal/terminal/session.py +503 -0
- pal/terminal/setup.py +354 -0
- pal/terminal/syntax.py +98 -0
- pal/terminal/theme.py +37 -0
- pal/terminal/ui.py +647 -0
- pal/terminal/voice.py +137 -0
- pal/terminal/workflow.py +624 -0
- pal/token_tracker/__init__.py +3 -0
- pal/token_tracker/pipeline.py +54 -0
- pal/tokens.py +21 -0
- pal/tool_catalog.py +107 -0
- pal/tool_manager/__init__.py +17 -0
- pal/tool_manager/builtin.py +82 -0
- pal/tool_manager/components/__init__.py +19 -0
- pal/tool_manager/components/base.py +53 -0
- pal/tool_manager/components/conflict_resolver.py +27 -0
- pal/tool_manager/components/param_validator.py +41 -0
- pal/tool_manager/components/tool_call_queue.py +111 -0
- pal/tool_manager/components/tool_registry.py +60 -0
- pal/tool_manager/pipeline.py +80 -0
- pal/tool_manager/utils/__init__.py +3 -0
- pal/web/__init__.py +3 -0
- pal/web/app.py +276 -0
- pal/web/static/editor.js +219 -0
- pal/web/static/fonts/LICENSE-Inter.txt +3 -0
- pal/web/static/fonts/inter-400.woff2 +0 -0
- pal/web/static/fonts/inter-500.woff2 +0 -0
- pal/web/static/fonts/inter-600.woff2 +0 -0
- pal/web/static/fonts/inter-700.woff2 +0 -0
- pal/web/static/inter.css +5 -0
- pal/web/static/pal-logo-256.png +0 -0
- pal/web/static/pal-logo-32.png +0 -0
- pal/web/static/pal-logo-64.png +0 -0
- pal/web/static/studio.css +306 -0
- pal/web/static/studio.js +533 -0
- pal/web/static/voice.js +76 -0
- pal/web/templates/index.html +368 -0
- pal/web/templates/studio.html +120 -0
- pal_code-0.1.0.dist-info/METADATA +379 -0
- pal_code-0.1.0.dist-info/RECORD +282 -0
- pal_code-0.1.0.dist-info/WHEEL +5 -0
- pal_code-0.1.0.dist-info/entry_points.txt +2 -0
- pal_code-0.1.0.dist-info/licenses/LICENSE +21 -0
- pal_code-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pal.compression.components.adverbs import Adverbs
|
|
4
|
+
from pal.compression.components.hedges import Hedges
|
|
5
|
+
from pal.compression.components.interjections import Interjections
|
|
6
|
+
from pal.compression.components.pleonasms import Pleonasms
|
|
7
|
+
from pal.compression.components.punctuations import Punctuations
|
|
8
|
+
from pal.compression.components.stopwords import Stopwords
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TextMinifier:
|
|
12
|
+
"""Strips hedges, pleonasms, interjections, filler adverbs, and
|
|
13
|
+
stopwords, then normalizes punctuation. Real input-side token
|
|
14
|
+
reduction, applied before a message is ever sent to an LLM core."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
hedges: Hedges | None = None,
|
|
19
|
+
pleonasms: Pleonasms | None = None,
|
|
20
|
+
interjections: Interjections | None = None,
|
|
21
|
+
adverbs: Adverbs | None = None,
|
|
22
|
+
stopwords: Stopwords | None = None,
|
|
23
|
+
punctuations: Punctuations | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
self.hedges = hedges or Hedges()
|
|
26
|
+
self.pleonasms = pleonasms or Pleonasms()
|
|
27
|
+
self.interjections = interjections or Interjections()
|
|
28
|
+
self.adverbs = adverbs or Adverbs()
|
|
29
|
+
self.stopwords = stopwords or Stopwords()
|
|
30
|
+
self.punctuations = punctuations or Punctuations()
|
|
31
|
+
|
|
32
|
+
def _remove_hedges(self, text: str) -> str:
|
|
33
|
+
return self.hedges.remove(text)
|
|
34
|
+
|
|
35
|
+
def _remove_pleonasms(self, text: str) -> str:
|
|
36
|
+
return self.pleonasms.remove(text)
|
|
37
|
+
|
|
38
|
+
def _remove_interjections(self, text: str) -> str:
|
|
39
|
+
return self.interjections.remove(text)
|
|
40
|
+
|
|
41
|
+
def _remove_adverbs(self, text: str) -> str:
|
|
42
|
+
return self.adverbs.remove(text)
|
|
43
|
+
|
|
44
|
+
def _remove_stopwords(self, text: str) -> str:
|
|
45
|
+
return self.stopwords.remove(text)
|
|
46
|
+
|
|
47
|
+
def _normalize_punctuations(self, text: str) -> str:
|
|
48
|
+
return self.punctuations.remove(text)
|
|
49
|
+
|
|
50
|
+
def minify(self, text: str) -> str:
|
|
51
|
+
without_hedges = self._remove_hedges(text)
|
|
52
|
+
without_pleonasms = self._remove_pleonasms(without_hedges)
|
|
53
|
+
without_interjections = self._remove_interjections(without_pleonasms)
|
|
54
|
+
without_adverbs = self._remove_adverbs(without_interjections)
|
|
55
|
+
without_stopwords = self._remove_stopwords(without_adverbs)
|
|
56
|
+
return self._normalize_punctuations(without_stopwords)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class OutputCompressor:
|
|
5
|
+
def __init__(self, max_blank_lines: int = 1, trim_trailing: bool = True):
|
|
6
|
+
self.max_blank_lines = max_blank_lines
|
|
7
|
+
self.trim_trailing = trim_trailing
|
|
8
|
+
|
|
9
|
+
def compress(self, text: str) -> str:
|
|
10
|
+
out = re.sub(r"\n{" + str(self.max_blank_lines + 2) + ",}", "\n" * (self.max_blank_lines + 1), text)
|
|
11
|
+
if self.trim_trailing:
|
|
12
|
+
out = "\n".join(line.rstrip() for line in out.split("\n"))
|
|
13
|
+
return out.strip()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from proselint.tools import DEFAULT, LintFile
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Pleonasms:
|
|
7
|
+
RELEVANT_PREFIX: str = "redundancy"
|
|
8
|
+
|
|
9
|
+
def _lint(self, text: str) -> list:
|
|
10
|
+
lint_file = LintFile("<memory>", text)
|
|
11
|
+
return [
|
|
12
|
+
result
|
|
13
|
+
for result in lint_file.lint(DEFAULT)
|
|
14
|
+
if result.check_result.check_path.startswith(self.RELEVANT_PREFIX)
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
def _extract_edits(self, results: list) -> list[tuple[int, int, str | None]]:
|
|
18
|
+
return sorted(
|
|
19
|
+
(
|
|
20
|
+
(
|
|
21
|
+
result.check_result.span[0] - 1,
|
|
22
|
+
result.check_result.span[1] - 1,
|
|
23
|
+
result.check_result.replacements,
|
|
24
|
+
)
|
|
25
|
+
for result in results
|
|
26
|
+
),
|
|
27
|
+
key=lambda edit: (edit[0], -(edit[1] - edit[0])),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def _resolve_overlaps(
|
|
31
|
+
self, edits: list[tuple[int, int, str | None]]
|
|
32
|
+
) -> list[tuple[int, int, str | None]]:
|
|
33
|
+
resolved: list[tuple[int, int, str | None]] = []
|
|
34
|
+
last_end = -1
|
|
35
|
+
for start, end, replacement in edits:
|
|
36
|
+
if start < last_end:
|
|
37
|
+
continue
|
|
38
|
+
resolved.append((start, end, replacement))
|
|
39
|
+
last_end = end
|
|
40
|
+
return resolved
|
|
41
|
+
|
|
42
|
+
def _apply_edits(
|
|
43
|
+
self, text: str, edits: list[tuple[int, int, str | None]]
|
|
44
|
+
) -> str:
|
|
45
|
+
cleaned = text
|
|
46
|
+
for start, end, replacement in reversed(edits):
|
|
47
|
+
cleaned = cleaned[:start] + (replacement or "") + cleaned[end:]
|
|
48
|
+
return cleaned
|
|
49
|
+
|
|
50
|
+
def remove(self, text: str) -> str:
|
|
51
|
+
results = self._lint(text)
|
|
52
|
+
edits = self._extract_edits(results)
|
|
53
|
+
resolved_edits = self._resolve_overlaps(edits)
|
|
54
|
+
return self._apply_edits(text, resolved_edits)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Punctuations:
|
|
7
|
+
def _space_around_marks(self, text: str) -> str:
|
|
8
|
+
return re.sub(r"\s*([,.!?;:])\s*", r"\1 ", text)
|
|
9
|
+
|
|
10
|
+
def _collapse_repeated_marks(self, text: str) -> str:
|
|
11
|
+
return re.sub(r"([,.!?;:]\s*){2,}", r"\1", text)
|
|
12
|
+
|
|
13
|
+
def _remove_comma_before_terminal_mark(self, text: str) -> str:
|
|
14
|
+
return re.sub(r",(\s*[.!?])", r"\1", text)
|
|
15
|
+
|
|
16
|
+
def _trim_leading_marks(self, text: str) -> str:
|
|
17
|
+
return re.sub(r"^[\s,.!?;:]+", "", text)
|
|
18
|
+
|
|
19
|
+
def _trim_trailing_marks(self, text: str) -> str:
|
|
20
|
+
return re.sub(r"[\s,.!?;:]+$", "", text)
|
|
21
|
+
|
|
22
|
+
def _normalize_whitespace(self, text: str) -> str:
|
|
23
|
+
return re.sub(r"\s+", " ", text).strip()
|
|
24
|
+
|
|
25
|
+
def _remove_trailing_period(self, text: str) -> str:
|
|
26
|
+
return re.sub(r"\.$", "", text)
|
|
27
|
+
|
|
28
|
+
def remove(self, text: str) -> str:
|
|
29
|
+
text = self._space_around_marks(text)
|
|
30
|
+
text = self._collapse_repeated_marks(text)
|
|
31
|
+
text = self._remove_comma_before_terminal_mark(text)
|
|
32
|
+
text = self._trim_leading_marks(text)
|
|
33
|
+
text = self._trim_trailing_marks(text)
|
|
34
|
+
text = self._normalize_whitespace(text)
|
|
35
|
+
text = self._remove_trailing_period(text)
|
|
36
|
+
return text
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import nltk
|
|
4
|
+
from nltk.corpus import stopwords as nltk_stopwords
|
|
5
|
+
|
|
6
|
+
from pal.compression.utils.negations import Negations
|
|
7
|
+
from pal.compression.utils.pronouns import Pronouns
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Stopwords:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
pronouns: Pronouns | None = None,
|
|
14
|
+
negations: Negations | None = None,
|
|
15
|
+
exclude_contractions: bool = True,
|
|
16
|
+
) -> None:
|
|
17
|
+
self.pronouns = pronouns or Pronouns()
|
|
18
|
+
self.negations = negations or Negations()
|
|
19
|
+
self.words = self._build_stop_words(exclude_contractions)
|
|
20
|
+
|
|
21
|
+
def _build_stop_words(self, exclude_contractions: bool) -> set[str]:
|
|
22
|
+
raw_stop_words = set(nltk_stopwords.words("english"))
|
|
23
|
+
if exclude_contractions:
|
|
24
|
+
return {word for word in raw_stop_words if "'" not in word}
|
|
25
|
+
return raw_stop_words
|
|
26
|
+
|
|
27
|
+
def _tokenize(self, text: str) -> list[str]:
|
|
28
|
+
return text.split()
|
|
29
|
+
|
|
30
|
+
def _tag_tokens(self, tokens: list[str]) -> list[tuple[str, str]]:
|
|
31
|
+
return nltk.pos_tag(tokens)
|
|
32
|
+
|
|
33
|
+
def _is_stop_word(self, word: str) -> bool:
|
|
34
|
+
return word.lower() in self.words
|
|
35
|
+
|
|
36
|
+
def _is_protected(self, word: str, tag: str) -> bool:
|
|
37
|
+
if self.pronouns.is_pronoun(word, tag):
|
|
38
|
+
return True
|
|
39
|
+
return self.negations.is_negation(word)
|
|
40
|
+
|
|
41
|
+
def _is_removable(self, word: str, tag: str) -> bool:
|
|
42
|
+
if self._is_protected(word, tag):
|
|
43
|
+
return False
|
|
44
|
+
return self._is_stop_word(word)
|
|
45
|
+
|
|
46
|
+
def _filter_tokens(
|
|
47
|
+
self, tagged_tokens: list[tuple[str, str]]
|
|
48
|
+
) -> list[tuple[str, str]]:
|
|
49
|
+
return [
|
|
50
|
+
(word, tag)
|
|
51
|
+
for word, tag in tagged_tokens
|
|
52
|
+
if not self._is_removable(word, tag)
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
def _join_tokens(self, tagged_tokens: list[tuple[str, str]]) -> str:
|
|
56
|
+
return " ".join(word for word, _ in tagged_tokens)
|
|
57
|
+
|
|
58
|
+
def remove(self, text: str) -> str:
|
|
59
|
+
tokens = self._tokenize(text)
|
|
60
|
+
tagged_tokens = self._tag_tokens(tokens)
|
|
61
|
+
filtered_tokens = self._filter_tokens(tagged_tokens)
|
|
62
|
+
return self._join_tokens(filtered_tokens)
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""The Compression component for conversation history: it shortens OLD assistant replies, with no model and no new dependency.
|
|
2
|
+
|
|
3
|
+
What is compressed: only the assistant's earlier replies (the bulk of a long agent session: code and drafts). What is never
|
|
4
|
+
compressed: the system prompt, your own messages, the newest turns, and anything that is an exact detail (numbers, names, file
|
|
5
|
+
names and paths, URLs, `code` spans, "quoted" strings, error messages).
|
|
6
|
+
|
|
7
|
+
How, by kind of content:
|
|
8
|
+
* a code block -> its signature lines (def, class, function ...) stay, the body becomes "[... N lines omitted]";
|
|
9
|
+
* an earlier draft that a later reply replaced (same functions, same headings) -> a one-line pointer to the newer turn;
|
|
10
|
+
* a reply that saved a file -> a pointer to the file plus its first sentence;
|
|
11
|
+
* prose -> the first and last sentence, and every sentence holding an exact detail or a decision word.
|
|
12
|
+
Nothing is deleted: the original turn stays as it was, and a relevant old turn is still recalled in full. A turn that cannot be
|
|
13
|
+
made shorter is returned untouched. `extract_facts` keeps the exact details of turns that are too old to show at all.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
from pal.tokens import estimate_tokens
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from pal.config import ContextConfig
|
|
26
|
+
from pal.context_manager.components.turns import PagedHistory, Turn
|
|
27
|
+
|
|
28
|
+
MARK = "[compressed] "
|
|
29
|
+
|
|
30
|
+
_FENCE = re.compile(r"```([^\n`]*)\n(.*?)```", re.S)
|
|
31
|
+
_MODS = r"(?:export[ \t]+|async[ \t]+|public[ \t]+|private[ \t]+|protected[ \t]+|static[ \t]+|pub[ \t]+)*"
|
|
32
|
+
_DEF = re.compile(rf"[ \t]*{_MODS}(?:def|class|function|func|fn|interface|struct|enum|trait|type)[ \t]+([A-Za-z_$][\w$]*)")
|
|
33
|
+
_ARROW = re.compile(r"[ \t]*(?:export[ \t]+)?(?:const|let|var)[ \t]+([A-Za-z_$][\w$]*)[ \t]*=.*=>")
|
|
34
|
+
_HEADING = re.compile(r"^#{1,6}[ \t]+(.+?)[ \t]*$", re.M)
|
|
35
|
+
_BULLET = re.compile(r"\s*(?:[-*\u2022]|\d+[.)])\s")
|
|
36
|
+
_SENTENCE_END = re.compile(r"(?<=[.!?])\s+(?=[A-Z0-9`\"'(\[])")
|
|
37
|
+
|
|
38
|
+
_FILE = re.compile(r"(?<![\w/.-])(?:[\w.-]+/)*[\w.-]+\.(?:py|js|jsx|ts|tsx|md|json|ya?ml|toml|java|kt|go|rs|c|cc|cpp|h|hpp|css|html|sh|txt|csv|sql|ini|cfg|lock|xml)\b")
|
|
39
|
+
_URL = re.compile(r"https?://[^\s)>\]]+")
|
|
40
|
+
_TICK = re.compile(r"`([^`\n]{1,40})`")
|
|
41
|
+
_QUOTE = re.compile(r"[\"\u201c]([^\"\u201d\n]{2,50})[\"\u201d]")
|
|
42
|
+
_ERROR = re.compile(r"\b[A-Z][A-Za-z]*(?:Error|Exception)\b|\bHTTP\s?\d{3}\b")
|
|
43
|
+
_DIGIT = re.compile(r"\d")
|
|
44
|
+
_ERRORWORD = re.compile(r"\b(?:error|exception|traceback|failed|failure|warning|deprecated)\b", re.I)
|
|
45
|
+
_DECISION = re.compile(r"\b(?:decid\w*|chose|because|instead of|must|should|recommend\w*|avoid|do not|don't|never|always|fixed?)\b", re.I)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _is_exact(unit: str) -> bool:
|
|
49
|
+
"""Does this sentence hold something that must survive verbatim?"""
|
|
50
|
+
return bool(_DIGIT.search(unit) or _FILE.search(unit) or _URL.search(unit) or _TICK.search(unit) or _QUOTE.search(unit)
|
|
51
|
+
or _ERRORWORD.search(unit) or _ERROR.search(unit))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _segments(text: str) -> list[tuple[str, str, str]]:
|
|
55
|
+
"""[(kind, lang, text)] with kind "prose" or "code"."""
|
|
56
|
+
out, pos = [], 0
|
|
57
|
+
for m in _FENCE.finditer(text):
|
|
58
|
+
if text[pos:m.start()].strip():
|
|
59
|
+
out.append(("prose", "", text[pos:m.start()]))
|
|
60
|
+
out.append(("code", m.group(1).strip(), m.group(2)))
|
|
61
|
+
pos = m.end()
|
|
62
|
+
if text[pos:].strip():
|
|
63
|
+
out.append(("prose", "", text[pos:]))
|
|
64
|
+
return out
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def defined_names(code: str) -> list[str]:
|
|
68
|
+
names = []
|
|
69
|
+
for line in code.splitlines():
|
|
70
|
+
m = _DEF.match(line) or _ARROW.match(line)
|
|
71
|
+
if m:
|
|
72
|
+
names.append(m.group(1))
|
|
73
|
+
return names
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def shorten_code(lang: str, body: str, keep_other: int = 3) -> str:
|
|
77
|
+
"""Keep signatures and drop the body. A block of four lines or fewer is left alone."""
|
|
78
|
+
lines = body.splitlines()
|
|
79
|
+
if len(lines) <= 4:
|
|
80
|
+
return f"```{lang}\n{body}```"
|
|
81
|
+
sigs = [ln for ln in lines if _DEF.match(ln) or _ARROW.match(ln)][:12]
|
|
82
|
+
kept = sigs or lines[:keep_other]
|
|
83
|
+
omitted = len(lines) - len(kept)
|
|
84
|
+
return f"```{lang}\n" + "\n".join(kept) + f"\n[... {omitted} lines omitted]\n```"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _units(text: str) -> list[str]:
|
|
88
|
+
units: list[str] = []
|
|
89
|
+
for line in text.splitlines():
|
|
90
|
+
line = line.strip()
|
|
91
|
+
if not line:
|
|
92
|
+
continue
|
|
93
|
+
if _BULLET.match(line):
|
|
94
|
+
units.append(line)
|
|
95
|
+
else:
|
|
96
|
+
units.extend(s.strip() for s in _SENTENCE_END.split(line) if s.strip())
|
|
97
|
+
return units
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def compress_prose(text: str) -> str:
|
|
101
|
+
"""First and last sentence, plus every sentence with an exact detail or a decision word. Unchanged if that saves under 15%."""
|
|
102
|
+
units = _units(text)
|
|
103
|
+
if len(units) <= 2:
|
|
104
|
+
return text.strip()
|
|
105
|
+
keep = [i in (0, len(units) - 1) or _is_exact(u) or bool(_DECISION.search(u)) for i, u in enumerate(units)]
|
|
106
|
+
out = " ".join(u for u, k in zip(units, keep) if k)
|
|
107
|
+
return out if len(out.split()) < 0.85 * len(text.split()) else text.strip()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# -- what a reply is "about", to notice that a later reply replaced it ----------------------------------------------------------------
|
|
111
|
+
def _keys(turn: Turn) -> tuple[frozenset, frozenset, list]:
|
|
112
|
+
"""(function and class names defined in its code, its headings lower-cased, its headings in document order)."""
|
|
113
|
+
if turn._keys is None:
|
|
114
|
+
text = turn.assistant
|
|
115
|
+
names = frozenset(n for _, _, body in (s for s in _segments(text) if s[0] == "code") for n in defined_names(body))
|
|
116
|
+
ordered = [h.strip() for h in _HEADING.findall(text)]
|
|
117
|
+
turn._keys = (names, frozenset(h.lower() for h in ordered), ordered)
|
|
118
|
+
return turn._keys
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _replaced_by(turns: list[Turn], i: int) -> tuple[int, list[str]] | None:
|
|
122
|
+
"""(later turn index, the earlier draft's headings) if a later reply is a newer version of the whole draft in turn i."""
|
|
123
|
+
_, heads, ordered = _keys(turns[i])
|
|
124
|
+
if len(heads) < 2:
|
|
125
|
+
return None
|
|
126
|
+
for j in range(i + 1, len(turns)):
|
|
127
|
+
later = _keys(turns[j])[1]
|
|
128
|
+
if later and len(heads & later) / len(heads) >= 0.6:
|
|
129
|
+
return j, ordered[:3]
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _block_replaced_by(turns: list[Turn], i: int, names: list[str]) -> int | None:
|
|
134
|
+
if not names:
|
|
135
|
+
return None
|
|
136
|
+
for j in range(i + 1, len(turns)):
|
|
137
|
+
later = _keys(turns[j])[0]
|
|
138
|
+
if later and len(set(names) & later) / len(set(names)) >= 0.5:
|
|
139
|
+
return j
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def compact_reply(turns: list[Turn], i: int, min_words: int = 60, count: Callable[[str], int] = estimate_tokens) -> str:
|
|
144
|
+
"""The compressed form of turn i's assistant reply, or the original text if compressing would not make it shorter."""
|
|
145
|
+
turn = turns[i]
|
|
146
|
+
text = turn.assistant
|
|
147
|
+
if turn.files:
|
|
148
|
+
first = " ".join(_units(" ".join(s[2] for s in _segments(text) if s[0] == "prose") or text)[:1]).split()
|
|
149
|
+
out = f"{MARK}[saved {', '.join(turn.files)} in the workspace] " + " ".join(first[:25])
|
|
150
|
+
return out.strip() if count(out) < count(text) else text
|
|
151
|
+
if len(text.split()) < min_words:
|
|
152
|
+
return text
|
|
153
|
+
replaced = _replaced_by(turns, i)
|
|
154
|
+
if replaced:
|
|
155
|
+
j, heads = replaced
|
|
156
|
+
title = ", ".join(f'"{h}"' for h in heads)
|
|
157
|
+
out = f"{MARK}[earlier draft with the headings {title}, replaced by the newer version in turn {j + 1}]"
|
|
158
|
+
return out if count(out) < count(text) else text
|
|
159
|
+
parts = []
|
|
160
|
+
for kind, lang, seg in _segments(text):
|
|
161
|
+
if kind == "code":
|
|
162
|
+
names = defined_names(seg)
|
|
163
|
+
newer = _block_replaced_by(turns, i, names)
|
|
164
|
+
parts.append(f"[earlier version of {', '.join(names[:4])}, replaced by the newer one in turn {newer + 1}]" if newer is not None
|
|
165
|
+
else shorten_code(lang, seg))
|
|
166
|
+
else:
|
|
167
|
+
parts.append(compress_prose(seg))
|
|
168
|
+
out = MARK + "\n".join(p for p in parts if p)
|
|
169
|
+
return out if count(out) < count(text) else text
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# -- exact details of turns too old to show at all ----------------------------------------------------------------------------------------
|
|
173
|
+
# A number labelled like these is a counter or an ordinal, not a fact worth keeping ("attempt 7", "version 3", "step 2").
|
|
174
|
+
_ORDINAL = frozenset({"attempt", "version", "revision", "step", "turn", "number", "question", "item", "part", "page", "chapter",
|
|
175
|
+
"section", "no", "rev", "v", "draft", "round", "try", "line", "lines", "figure", "table", "example"})
|
|
176
|
+
_UNIT = re.compile(r"^(?:dollars?|euros?|usd|eur|gbp|inr|rupees?|rs|percent|ms|secs?|seconds?|minutes?|hours?|days?|weeks?|months?|"
|
|
177
|
+
r"years?|kb|mb|gb|tb|tokens?|rows?|users?|requests?|files?|items?|people|calls?|ports?)\b", re.I)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _strong_number(token: str, nxt: str) -> bool:
|
|
181
|
+
"""Is this number worth keeping from an ASSISTANT reply? A currency, a percentage, three or more digits, or a unit after it."""
|
|
182
|
+
return bool(re.search(r"[$\u20ac\u00a3\u20b9%]", token) or len(re.sub(r"\D", "", token)) >= 3 or _UNIT.match(nxt))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def extract_facts(text: str, limit: int = 12, strict: bool = False) -> list[str]:
|
|
186
|
+
"""Numbers (with the few words around them), file names, URLs, `code` spans, "quoted" strings and error names. No model.
|
|
187
|
+
`strict` (used for assistant replies) keeps only numbers that look like real quantities."""
|
|
188
|
+
prose = " ".join(s[2] for s in _segments(text) if s[0] == "prose") if "```" in text else text
|
|
189
|
+
seen: set[str] = set()
|
|
190
|
+
numbers: list[str] = []
|
|
191
|
+
others: list[str] = []
|
|
192
|
+
|
|
193
|
+
def add(bucket: list[str], s: str) -> None:
|
|
194
|
+
s = " ".join(s.split())[:80].strip(" ,;:.()")
|
|
195
|
+
if s and s.lower() not in seen:
|
|
196
|
+
seen.add(s.lower())
|
|
197
|
+
bucket.append(s)
|
|
198
|
+
|
|
199
|
+
for unit in _units(prose):
|
|
200
|
+
words = unit.split()
|
|
201
|
+
for k, w in enumerate(words):
|
|
202
|
+
if not _DIGIT.search(w) or (k > 0 and words[k - 1].lower().strip(":,.") in _ORDINAL):
|
|
203
|
+
continue
|
|
204
|
+
if k >= 2 and words[k - 1].lower() == "of" and _DIGIT.search(words[k - 2]): # "2 of 5": a position, not a fact
|
|
205
|
+
continue
|
|
206
|
+
if strict and not _strong_number(w, words[k + 1] if k + 1 < len(words) else ""):
|
|
207
|
+
continue
|
|
208
|
+
add(numbers, " ".join(words[max(0, k - 3):k + 2]))
|
|
209
|
+
for pat in (_FILE, _URL, _ERROR):
|
|
210
|
+
for m in pat.finditer(prose):
|
|
211
|
+
add(others, m.group(0))
|
|
212
|
+
for pat in (_TICK, _QUOTE):
|
|
213
|
+
for m in pat.finditer(prose):
|
|
214
|
+
add(others, m.group(1))
|
|
215
|
+
return (numbers[:6] + others[:6])[:limit] if len(numbers) > 6 else (numbers + others)[:limit]
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def turn_fact_split(turn: Turn, limit: int = 12) -> tuple[list[str], list[str]]:
|
|
219
|
+
"""(details from what the USER said, details from the assistant's reply). The user's own words come first when space is short:
|
|
220
|
+
they are the requirements and the figures that were given."""
|
|
221
|
+
if turn._facts is None:
|
|
222
|
+
user = extract_facts(turn.user, limit)
|
|
223
|
+
have = {f.lower() for f in user}
|
|
224
|
+
asst = [f for f in extract_facts(turn.assistant, limit, strict=True) if f.lower() not in have]
|
|
225
|
+
turn._facts = (user, asst)
|
|
226
|
+
return turn._facts
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def turn_facts(turn: Turn, limit: int = 12) -> list[str]:
|
|
230
|
+
user, asst = turn_fact_split(turn, limit)
|
|
231
|
+
return (user + asst)[:limit]
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def facts_block(turns: list[Turn], indexes: list[int], budget_tokens: int, count: Callable[[str], int] = estimate_tokens) -> tuple[str, int]:
|
|
235
|
+
"""(block, tokens): the exact details of the turns at `indexes`, oldest first.
|
|
236
|
+
|
|
237
|
+
A fact that appears in several turns is listed once, at its most recent turn. When space is short, details from the user's
|
|
238
|
+
own messages are kept first (newest first), then details from assistant replies (newest first)."""
|
|
239
|
+
seen: set[str] = set()
|
|
240
|
+
user_by: dict[int, list[str]] = {}
|
|
241
|
+
asst_by: dict[int, list[str]] = {}
|
|
242
|
+
for i in sorted(indexes, reverse=True): # newest first, so a repeated fact stays at its latest turn
|
|
243
|
+
user, asst = turn_fact_split(turns[i])
|
|
244
|
+
u = [f for f in user if f.lower() not in seen]
|
|
245
|
+
seen.update(f.lower() for f in u)
|
|
246
|
+
a = [f for f in asst if f.lower() not in seen]
|
|
247
|
+
seen.update(f.lower() for f in a)
|
|
248
|
+
if u:
|
|
249
|
+
user_by[i] = u
|
|
250
|
+
if a:
|
|
251
|
+
asst_by[i] = a
|
|
252
|
+
head = "<facts>\nExact details from older turns (not shown):"
|
|
253
|
+
used = count(head) + count("\n</facts>")
|
|
254
|
+
chosen_u: dict[int, list[str]] = {}
|
|
255
|
+
chosen_a: dict[int, list[str]] = {}
|
|
256
|
+
for i in sorted(user_by, reverse=True):
|
|
257
|
+
cost = count(f"t{i + 1}: " + "; ".join(user_by[i])) + 1
|
|
258
|
+
if used + cost > budget_tokens:
|
|
259
|
+
break
|
|
260
|
+
chosen_u[i] = user_by[i]
|
|
261
|
+
used += cost
|
|
262
|
+
for i in sorted(asst_by, reverse=True):
|
|
263
|
+
for f in asst_by[i]:
|
|
264
|
+
cost = count(f + "; ") + (0 if i in chosen_u or i in chosen_a else count(f"t{i + 1}: ") + 1)
|
|
265
|
+
if used + cost > budget_tokens:
|
|
266
|
+
continue
|
|
267
|
+
chosen_a.setdefault(i, []).append(f)
|
|
268
|
+
used += cost
|
|
269
|
+
lines = [f"t{i + 1}: " + "; ".join(chosen_u.get(i, []) + chosen_a.get(i, [])) for i in sorted(set(chosen_u) | set(chosen_a))]
|
|
270
|
+
if not lines:
|
|
271
|
+
return "", 0
|
|
272
|
+
return head + "\n" + "\n".join(lines) + "\n</facts>", used
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def composition(turns: list[Turn], count: Callable[[str], int] = estimate_tokens) -> dict:
|
|
276
|
+
"""What is big? Tokens of the whole history split into your messages, assistant prose and assistant code."""
|
|
277
|
+
user = prose = code = 0
|
|
278
|
+
for t in turns:
|
|
279
|
+
user += count(t.user)
|
|
280
|
+
for kind, _, seg in _segments(t.assistant):
|
|
281
|
+
if kind == "code":
|
|
282
|
+
code += count(seg)
|
|
283
|
+
else:
|
|
284
|
+
prose += count(seg)
|
|
285
|
+
return {"user": user, "assistant_prose": prose, "assistant_code": code}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# -- the layering of a conversation by age, which used to live inside the context manager ---------------------------------------------------
|
|
289
|
+
@dataclass
|
|
290
|
+
class LayeredHistory:
|
|
291
|
+
"""The history after compression: what to show (chronological, some turns are compressed copies) and what became only facts."""
|
|
292
|
+
history: list # turns to show
|
|
293
|
+
compact_ids: set[int] = field(default_factory=set) # id() of the compressed copies among them
|
|
294
|
+
cold_idx: list[int] = field(default_factory=list) # positions of the turns too old to show, left to the facts block
|
|
295
|
+
warm_verbatim: int = 0 # tokens the compressed turns would have cost in full
|
|
296
|
+
warm_compact: int = 0 # tokens they cost compressed
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class HistoryCompressor:
|
|
300
|
+
"""Shows a conversation in layers by age. The newest turns (chosen by the pager) stay verbatim; the next `compress_turns` older ones keep
|
|
301
|
+
your message and show the assistant's reply compressed, within `compress_budget_tokens`; everything older becomes a block of exact facts.
|
|
302
|
+
Originals are never changed or deleted. No model is involved."""
|
|
303
|
+
|
|
304
|
+
def __init__(self, cfg: ContextConfig, turn_cost: Callable, count: Callable[[str], int] = estimate_tokens):
|
|
305
|
+
self.cfg, self.turn_cost, self.count = cfg, turn_cost, count
|
|
306
|
+
|
|
307
|
+
def layer(self, all_turns: list, selected: list, paged: PagedHistory | None) -> LayeredHistory:
|
|
308
|
+
from pal.context_manager.components.turns import Turn
|
|
309
|
+
out = LayeredHistory(list(selected))
|
|
310
|
+
if not (self.cfg.enabled and self.cfg.compress and paged and paged.dropped_turns):
|
|
311
|
+
return out
|
|
312
|
+
chosen = {id(t) for t in selected}
|
|
313
|
+
dropped = [i for i, t in enumerate(all_turns) if id(t) not in chosen]
|
|
314
|
+
n = self.cfg.compress_turns
|
|
315
|
+
candidates = dropped[len(dropped) - n:] if n else []
|
|
316
|
+
cold = dropped[:len(dropped) - len(candidates)]
|
|
317
|
+
by_pos = {id(t): i for i, t in enumerate(all_turns)}
|
|
318
|
+
entries = [(by_pos[id(t)], t) for t in selected]
|
|
319
|
+
spent = 0
|
|
320
|
+
for i in reversed(candidates): # newest first, so the budget goes to the most recent
|
|
321
|
+
t = all_turns[i]
|
|
322
|
+
text = compact_reply(all_turns, i, self.cfg.compress_min_words, self.count)
|
|
323
|
+
if text is t.assistant: # nothing to compress: showing it would only add tokens
|
|
324
|
+
cold.append(i)
|
|
325
|
+
continue
|
|
326
|
+
variant = Turn(t.user, text, local=t.local, ts=t.ts, files=t.files)
|
|
327
|
+
cost = self.turn_cost(variant)
|
|
328
|
+
if spent + cost > self.cfg.compress_budget_tokens:
|
|
329
|
+
cold.append(i)
|
|
330
|
+
continue
|
|
331
|
+
spent += cost
|
|
332
|
+
out.compact_ids.add(id(variant))
|
|
333
|
+
out.warm_verbatim += self.turn_cost(t)
|
|
334
|
+
out.warm_compact += cost
|
|
335
|
+
entries.append((i, variant))
|
|
336
|
+
out.cold_idx = sorted(cold)
|
|
337
|
+
out.history = [t for _, t in sorted(entries, key=lambda e: e[0])]
|
|
338
|
+
return out
|
|
339
|
+
|
|
340
|
+
def facts(self, all_turns: list, cold_idx: list[int]) -> str | None:
|
|
341
|
+
"""The facts block for the turns too old to show, or None when there is none or it would not be much smaller than those turns."""
|
|
342
|
+
if not cold_idx:
|
|
343
|
+
return None
|
|
344
|
+
text, tokens = facts_block(all_turns, cold_idx, self.cfg.facts_tokens, self.count)
|
|
345
|
+
stands_for = sum(self.turn_cost(all_turns[i]) for i in cold_idx)
|
|
346
|
+
return text if text and tokens <= self.cfg.facts_max_ratio * stands_for else None
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from pal.compression.components.output_compressor import OutputCompressor
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from pal.compression.components.minifier import TextMinifier
|
|
7
|
+
_MINIFIER_AVAILABLE = True
|
|
8
|
+
except Exception:
|
|
9
|
+
# NLTK/spaCy data not downloaded yet, or a dependency failed to
|
|
10
|
+
# import for some other reason. Fall back to the old whitespace/
|
|
11
|
+
# dedupe-only behavior rather than crashing the whole pipeline --
|
|
12
|
+
# real compression kicks in once install.sh's NLTK/spaCy download
|
|
13
|
+
# steps have run.
|
|
14
|
+
_MINIFIER_AVAILABLE = False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CompressionPipeline:
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
strip_whitespace: bool = True,
|
|
21
|
+
dedupe_lines: bool = True,
|
|
22
|
+
use_minifier: bool = True,
|
|
23
|
+
max_blank_lines: int = 1,
|
|
24
|
+
trim_trailing: bool = True,
|
|
25
|
+
):
|
|
26
|
+
self.strip_whitespace = strip_whitespace
|
|
27
|
+
self.dedupe_lines = dedupe_lines
|
|
28
|
+
self.use_minifier = use_minifier and _MINIFIER_AVAILABLE
|
|
29
|
+
self._minifier = TextMinifier() if self.use_minifier else None
|
|
30
|
+
self._output_compressor = OutputCompressor(max_blank_lines=max_blank_lines, trim_trailing=trim_trailing)
|
|
31
|
+
|
|
32
|
+
def compress_input(self, text: str) -> str:
|
|
33
|
+
out = text
|
|
34
|
+
|
|
35
|
+
# Real token-reducing step: strips hedges, pleonasms,
|
|
36
|
+
# interjections, filler adverbs, stopwords, and normalizes
|
|
37
|
+
# punctuation.
|
|
38
|
+
if self.use_minifier:
|
|
39
|
+
out = self._minifier.minify(out)
|
|
40
|
+
|
|
41
|
+
if self.strip_whitespace:
|
|
42
|
+
out = re.sub(r"[ \t]+", " ", out)
|
|
43
|
+
out = re.sub(r"\n{3,}", "\n\n", out)
|
|
44
|
+
out = out.strip()
|
|
45
|
+
if self.dedupe_lines:
|
|
46
|
+
seen = set()
|
|
47
|
+
lines = []
|
|
48
|
+
for line in out.split("\n"):
|
|
49
|
+
key = line.strip()
|
|
50
|
+
if key and key in seen:
|
|
51
|
+
continue
|
|
52
|
+
seen.add(key)
|
|
53
|
+
lines.append(line)
|
|
54
|
+
out = "\n".join(lines)
|
|
55
|
+
return out
|
|
56
|
+
|
|
57
|
+
def compress_output(self, text: str) -> str:
|
|
58
|
+
return self._output_compressor.compress(text)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Optional NLP-backed setup helpers -- see components/__init__.py for
|
|
2
|
+
why each import here is individually guarded rather than allowed to
|
|
3
|
+
raise straight out of this package's __init__."""
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from pal.compression.utils.negations import Negations
|
|
7
|
+
except ImportError:
|
|
8
|
+
Negations = None
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from pal.compression.utils.nltk_setup import NltkSetup
|
|
12
|
+
except ImportError:
|
|
13
|
+
NltkSetup = None
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from pal.compression.utils.proselint_setup import ProselintSetup
|
|
17
|
+
except ImportError:
|
|
18
|
+
ProselintSetup = None
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from pal.compression.utils.pronouns import Pronouns
|
|
22
|
+
except ImportError:
|
|
23
|
+
Pronouns = None
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
from pal.compression.utils.spacy_setup import SpacySetup
|
|
27
|
+
except ImportError:
|
|
28
|
+
SpacySetup = None
|
|
29
|
+
|
|
30
|
+
__all__ = ["Negations", "NltkSetup", "ProselintSetup", "Pronouns", "SpacySetup"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Negations:
|
|
5
|
+
WORDS: set[str] = {
|
|
6
|
+
"no", "not", "nor", "never", "none", "nobody", "nothing",
|
|
7
|
+
"nowhere", "neither", "without", "cannot", "cant", "wont",
|
|
8
|
+
"dont", "isnt", "arent", "wasnt", "werent", "hasnt",
|
|
9
|
+
"havent", "hadnt", "doesnt", "didnt", "wouldnt", "shouldnt",
|
|
10
|
+
"couldnt",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
def _strip_apostrophes(self, word: str) -> str:
|
|
14
|
+
return word.replace("'", "")
|
|
15
|
+
|
|
16
|
+
def is_negation(self, word: str) -> bool:
|
|
17
|
+
normalized = self._strip_apostrophes(word.lower())
|
|
18
|
+
return normalized in self.WORDS
|