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.
Files changed (282) hide show
  1. pal/__init__.py +29 -0
  2. pal/__main__.py +3 -0
  3. pal/access_manager/__init__.py +16 -0
  4. pal/access_manager/components/__init__.py +14 -0
  5. pal/access_manager/components/access_control.py +41 -0
  6. pal/access_manager/components/base.py +39 -0
  7. pal/access_manager/components/intervention.py +31 -0
  8. pal/access_manager/components/privilege_registry.py +30 -0
  9. pal/access_manager/pipeline.py +68 -0
  10. pal/access_manager/utils/__init__.py +2 -0
  11. pal/agent_constraints/__init__.py +3 -0
  12. pal/agent_constraints/components/AGENT_OUTPUT.md +13 -0
  13. pal/agent_constraints/components/__init__.py +3 -0
  14. pal/agent_constraints/components/constraints.py +22 -0
  15. pal/agent_constraints/pipeline.py +22 -0
  16. pal/agent_constraints/utils/__init__.py +2 -0
  17. pal/agents/__init__.py +12 -0
  18. pal/agents/coding.py +14 -0
  19. pal/agents/docs.py +15 -0
  20. pal/chat/__init__.py +4 -0
  21. pal/chat/demo.py +231 -0
  22. pal/chat/providers.py +23 -0
  23. pal/chat/service.py +591 -0
  24. pal/chat/services/__init__.py +26 -0
  25. pal/chat/services/base.py +142 -0
  26. pal/chat/services/data.py +142 -0
  27. pal/chat/services/media.py +123 -0
  28. pal/chat/services/web.py +60 -0
  29. pal/chat/sessions.py +108 -0
  30. pal/chat/tools.py +446 -0
  31. pal/chat/uploads.py +81 -0
  32. pal/cli.py +502 -0
  33. pal/compression/__init__.py +12 -0
  34. pal/compression/components/__init__.py +64 -0
  35. pal/compression/components/adverbs.py +20 -0
  36. pal/compression/components/hedges.py +54 -0
  37. pal/compression/components/interjections.py +20 -0
  38. pal/compression/components/minifier.py +56 -0
  39. pal/compression/components/output_compressor.py +13 -0
  40. pal/compression/components/pleonasms.py +54 -0
  41. pal/compression/components/punctuations.py +36 -0
  42. pal/compression/components/stopwords.py +62 -0
  43. pal/compression/history.py +346 -0
  44. pal/compression/pipeline.py +58 -0
  45. pal/compression/utils/__init__.py +30 -0
  46. pal/compression/utils/negations.py +18 -0
  47. pal/compression/utils/nltk_setup.py +20 -0
  48. pal/compression/utils/pronouns.py +27 -0
  49. pal/compression/utils/proselint_setup.py +9 -0
  50. pal/compression/utils/spacy_setup.py +22 -0
  51. pal/config/default.yaml +163 -0
  52. pal/config.py +504 -0
  53. pal/context_manager/__init__.py +7 -0
  54. pal/context_manager/chat_context.py +117 -0
  55. pal/context_manager/components/__init__.py +5 -0
  56. pal/context_manager/components/assembler.py +75 -0
  57. pal/context_manager/components/context_cache.py +45 -0
  58. pal/context_manager/components/kv_bridge.py +16 -0
  59. pal/context_manager/components/pager.py +48 -0
  60. pal/context_manager/components/simple_context_manager.py +56 -0
  61. pal/context_manager/components/turns.py +88 -0
  62. pal/context_manager/pipeline.py +27 -0
  63. pal/context_manager/utils/__init__.py +18 -0
  64. pal/context_manager/utils/hashing.py +5 -0
  65. pal/context_manager/utils/relevance.py +59 -0
  66. pal/context_manager/utils/storage.py +37 -0
  67. pal/documents/__init__.py +4 -0
  68. pal/documents/components/__init__.py +0 -0
  69. pal/documents/components/chunker.py +79 -0
  70. pal/documents/components/index.py +155 -0
  71. pal/documents/utils/__init__.py +1 -0
  72. pal/errors.py +40 -0
  73. pal/input_converters/__init__.py +8 -0
  74. pal/input_converters/components/__init__.py +0 -0
  75. pal/input_converters/components/base.py +38 -0
  76. pal/input_converters/components/pandoc_converter.py +87 -0
  77. pal/input_converters/components/pdf_converter.py +46 -0
  78. pal/input_converters/components/pptx_converter.py +52 -0
  79. pal/input_converters/components/xlsx_converter.py +38 -0
  80. pal/input_converters/pipeline.py +72 -0
  81. pal/input_converters/utils/__init__.py +1 -0
  82. pal/input_converters/workspace.py +56 -0
  83. pal/input_savers/INPUT_RULES.md +2 -0
  84. pal/input_savers/__init__.py +5 -0
  85. pal/input_savers/components/__init__.py +0 -0
  86. pal/input_savers/components/base.py +40 -0
  87. pal/input_savers/components/methods.py +41 -0
  88. pal/input_savers/components/verifier.py +39 -0
  89. pal/input_savers/pipeline.py +154 -0
  90. pal/input_savers/utils/__init__.py +0 -0
  91. pal/input_savers/utils/guards.py +94 -0
  92. pal/knowledge_graph/__init__.py +10 -0
  93. pal/knowledge_graph/components/__init__.py +0 -0
  94. pal/knowledge_graph/components/base.py +50 -0
  95. pal/knowledge_graph/components/builder.py +54 -0
  96. pal/knowledge_graph/components/retriever.py +100 -0
  97. pal/knowledge_graph/components/workspace.py +116 -0
  98. pal/knowledge_graph/pipeline.py +215 -0
  99. pal/knowledge_graph/utils/__init__.py +1 -0
  100. pal/latency_tracker/__init__.py +3 -0
  101. pal/latency_tracker/pipeline.py +86 -0
  102. pal/llm_scheduler/__init__.py +11 -0
  103. pal/llm_scheduler/api.py +137 -0
  104. pal/llm_scheduler/code.py +28 -0
  105. pal/llm_scheduler/core.py +92 -0
  106. pal/llm_scheduler/local.py +215 -0
  107. pal/llm_scheduler/pipeline.py +42 -0
  108. pal/llm_scheduler/types.py +92 -0
  109. pal/llm_wrapper/__init__.py +34 -0
  110. pal/llm_wrapper/adapter.py +316 -0
  111. pal/llm_wrapper/components/__init__.py +23 -0
  112. pal/llm_wrapper/components/adapter_types.py +90 -0
  113. pal/llm_wrapper/components/base.py +195 -0
  114. pal/llm_wrapper/components/features.py +104 -0
  115. pal/llm_wrapper/components/litellm_wrapper.py +215 -0
  116. pal/llm_wrapper/components/openrouter_wrapper.py +114 -0
  117. pal/llm_wrapper/modes.py +73 -0
  118. pal/llm_wrapper/pipeline.py +61 -0
  119. pal/llm_wrapper/utils/__init__.py +4 -0
  120. pal/llm_wrapper/utils/registry.py +49 -0
  121. pal/llm_wrapper/utils/token_counting.py +10 -0
  122. pal/local_llm/__init__.py +5 -0
  123. pal/local_llm/components/__init__.py +0 -0
  124. pal/local_llm/components/huggingface_local.py +217 -0
  125. pal/local_llm/components/local_llm.py +60 -0
  126. pal/logging_setup.py +18 -0
  127. pal/memory/__init__.py +7 -0
  128. pal/memory/components/__init__.py +6 -0
  129. pal/memory/components/lru_k.py +56 -0
  130. pal/memory/components/manager.py +98 -0
  131. pal/memory/components/note.py +54 -0
  132. pal/memory/components/storage_manager.py +60 -0
  133. pal/memory/pipeline.py +122 -0
  134. pal/memory/utils/__init__.py +3 -0
  135. pal/memory/utils/keyword_overlap.py +3 -0
  136. pal/memory_tracker/__init__.py +3 -0
  137. pal/memory_tracker/components/__init__.py +0 -0
  138. pal/memory_tracker/components/process.py +56 -0
  139. pal/memory_tracker/pipeline.py +44 -0
  140. pal/metrics/__init__.py +4 -0
  141. pal/metrics/components/__init__.py +4 -0
  142. pal/metrics/components/collector.py +140 -0
  143. pal/metrics/components/exporter.py +28 -0
  144. pal/metrics/pipeline.py +49 -0
  145. pal/metrics/utils/__init__.py +2 -0
  146. pal/notes_manager/__init__.py +5 -0
  147. pal/notes_manager/components/__init__.py +0 -0
  148. pal/notes_manager/components/privacy.py +14 -0
  149. pal/notes_manager/components/recall.py +31 -0
  150. pal/notes_manager/components/tidy.py +28 -0
  151. pal/notes_manager/pipeline.py +138 -0
  152. pal/orchestrator/__init__.py +26 -0
  153. pal/orchestrator/components/__init__.py +26 -0
  154. pal/orchestrator/components/agent.py +105 -0
  155. pal/orchestrator/components/cgroup_manager.py +209 -0
  156. pal/orchestrator/components/context_interrupt.py +220 -0
  157. pal/orchestrator/components/limits.py +9 -0
  158. pal/orchestrator/components/scheduler.py +69 -0
  159. pal/orchestrator/components/syscall_scheduler.py +200 -0
  160. pal/orchestrator/pipeline.py +228 -0
  161. pal/orchestrator/utils/__init__.py +4 -0
  162. pal/orchestrator/utils/device.py +164 -0
  163. pal/orchestrator/utils/protocol.py +32 -0
  164. pal/os_kernel/__init__.py +5 -0
  165. pal/os_kernel/filesystem.py +80 -0
  166. pal/os_kernel/network.py +146 -0
  167. pal/os_kernel/pipeline.py +175 -0
  168. pal/os_kernel/sysinfo.py +36 -0
  169. pal/paths.py +59 -0
  170. pal/pipeline.py +177 -0
  171. pal/py.typed +0 -0
  172. pal/redact.py +19 -0
  173. pal/sdk/__init__.py +33 -0
  174. pal/sdk/adapters.py +43 -0
  175. pal/sdk/agent.py +99 -0
  176. pal/sdk/components/__init__.py +14 -0
  177. pal/sdk/components/autogen_adapter.py +65 -0
  178. pal/sdk/components/autogen_client.py +98 -0
  179. pal/sdk/components/framework_adapter_base.py +69 -0
  180. pal/sdk/components/metagpt_adapter.py +46 -0
  181. pal/sdk/components/open_interpreter_adapter.py +47 -0
  182. pal/sdk/components/tool_catalog.py +44 -0
  183. pal/sdk/os_api.py +84 -0
  184. pal/sdk/patterns.py +129 -0
  185. pal/sdk/pipeline.py +240 -0
  186. pal/sdk/queries.py +68 -0
  187. pal/sdk/savers.py +88 -0
  188. pal/sdk/utils/__init__.py +2 -0
  189. pal/storage_manager/__init__.py +17 -0
  190. pal/storage_manager/components/__init__.py +19 -0
  191. pal/storage_manager/components/base.py +50 -0
  192. pal/storage_manager/components/local_file_backend.py +84 -0
  193. pal/storage_manager/components/storage_queue.py +91 -0
  194. pal/storage_manager/components/vector_store_backend.py +122 -0
  195. pal/storage_manager/pipeline.py +153 -0
  196. pal/storage_manager/utils/__init__.py +4 -0
  197. pal/storage_manager/utils/memory_adapter.py +60 -0
  198. pal/storage_manager/utils/sdk.py +93 -0
  199. pal/studio/__init__.py +0 -0
  200. pal/studio/builder.py +166 -0
  201. pal/studio/catalog.py +90 -0
  202. pal/studio/checks.py +129 -0
  203. pal/studio/clients.py +6 -0
  204. pal/studio/editor.py +137 -0
  205. pal/studio/files.py +6 -0
  206. pal/studio/models.py +6 -0
  207. pal/studio/plan.py +149 -0
  208. pal/studio/planner.py +205 -0
  209. pal/studio/policy.py +69 -0
  210. pal/studio/routes.py +254 -0
  211. pal/studio/runs.py +210 -0
  212. pal/task_router/__init__.py +11 -0
  213. pal/task_router/components/__init__.py +0 -0
  214. pal/task_router/components/answerer.py +37 -0
  215. pal/task_router/components/base.py +34 -0
  216. pal/task_router/components/classifier.py +40 -0
  217. pal/task_router/components/gates.py +23 -0
  218. pal/task_router/pipeline.py +61 -0
  219. pal/task_router/utils/__init__.py +1 -0
  220. pal/terminal/__init__.py +1 -0
  221. pal/terminal/backend.py +141 -0
  222. pal/terminal/bench.py +173 -0
  223. pal/terminal/cli.py +826 -0
  224. pal/terminal/clients.py +170 -0
  225. pal/terminal/commands.py +401 -0
  226. pal/terminal/decide.py +242 -0
  227. pal/terminal/files.py +55 -0
  228. pal/terminal/flow_cli.py +439 -0
  229. pal/terminal/hardware.py +199 -0
  230. pal/terminal/history.py +88 -0
  231. pal/terminal/keys.py +83 -0
  232. pal/terminal/local_models.py +261 -0
  233. pal/terminal/logo.py +117 -0
  234. pal/terminal/models.py +99 -0
  235. pal/terminal/policy.py +54 -0
  236. pal/terminal/project.py +413 -0
  237. pal/terminal/review.py +65 -0
  238. pal/terminal/security.py +239 -0
  239. pal/terminal/session.py +503 -0
  240. pal/terminal/setup.py +354 -0
  241. pal/terminal/syntax.py +98 -0
  242. pal/terminal/theme.py +37 -0
  243. pal/terminal/ui.py +647 -0
  244. pal/terminal/voice.py +137 -0
  245. pal/terminal/workflow.py +624 -0
  246. pal/token_tracker/__init__.py +3 -0
  247. pal/token_tracker/pipeline.py +54 -0
  248. pal/tokens.py +21 -0
  249. pal/tool_catalog.py +107 -0
  250. pal/tool_manager/__init__.py +17 -0
  251. pal/tool_manager/builtin.py +82 -0
  252. pal/tool_manager/components/__init__.py +19 -0
  253. pal/tool_manager/components/base.py +53 -0
  254. pal/tool_manager/components/conflict_resolver.py +27 -0
  255. pal/tool_manager/components/param_validator.py +41 -0
  256. pal/tool_manager/components/tool_call_queue.py +111 -0
  257. pal/tool_manager/components/tool_registry.py +60 -0
  258. pal/tool_manager/pipeline.py +80 -0
  259. pal/tool_manager/utils/__init__.py +3 -0
  260. pal/web/__init__.py +3 -0
  261. pal/web/app.py +276 -0
  262. pal/web/static/editor.js +219 -0
  263. pal/web/static/fonts/LICENSE-Inter.txt +3 -0
  264. pal/web/static/fonts/inter-400.woff2 +0 -0
  265. pal/web/static/fonts/inter-500.woff2 +0 -0
  266. pal/web/static/fonts/inter-600.woff2 +0 -0
  267. pal/web/static/fonts/inter-700.woff2 +0 -0
  268. pal/web/static/inter.css +5 -0
  269. pal/web/static/pal-logo-256.png +0 -0
  270. pal/web/static/pal-logo-32.png +0 -0
  271. pal/web/static/pal-logo-64.png +0 -0
  272. pal/web/static/studio.css +306 -0
  273. pal/web/static/studio.js +533 -0
  274. pal/web/static/voice.js +76 -0
  275. pal/web/templates/index.html +368 -0
  276. pal/web/templates/studio.html +120 -0
  277. pal_code-0.1.0.dist-info/METADATA +379 -0
  278. pal_code-0.1.0.dist-info/RECORD +282 -0
  279. pal_code-0.1.0.dist-info/WHEEL +5 -0
  280. pal_code-0.1.0.dist-info/entry_points.txt +2 -0
  281. pal_code-0.1.0.dist-info/licenses/LICENSE +21 -0
  282. 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