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,75 @@
1
+ """One token budget for everything that goes into a request besides the user's own message.
2
+
3
+ History, the exact details of old turns, remembered notes, document excerpts and code-graph excerpts each have their own cap, and together
4
+ they could add up to a lot. The assembler keeps the total under `budget` by dropping the least useful first: remembered notes, then
5
+ the facts, then the oldest turns (down to the last two), then document excerpts, then code excerpts, then the remaining turns
6
+ (never the last one). What the user asks about right now (excerpts that matched it) outranks older history.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Callable
11
+ from dataclasses import dataclass, field
12
+
13
+ from pal.context_manager.components.turns import Turn
14
+ from pal.tokens import estimate_tokens
15
+
16
+ # the order blocks are dropped in, least useful first
17
+ DROP_ORDER = ("memory", "facts", "documents", "graph")
18
+
19
+
20
+ @dataclass
21
+ class Block:
22
+ source: str # "facts" | "memory" | "documents" | "graph"
23
+ text: str
24
+ tokens: int = 0
25
+
26
+
27
+ @dataclass
28
+ class ContextReport:
29
+ budget: int
30
+ used: int
31
+ by_source: dict = field(default_factory=dict) # tokens per source, after fitting
32
+ dropped: list = field(default_factory=list) # block sources dropped to fit
33
+ turns_dropped: int = 0
34
+ tokens_dropped_from_turns: int = 0
35
+
36
+
37
+ class ContextAssembler:
38
+ def __init__(self, budget: int, count: Callable[[str], int] = estimate_tokens):
39
+ self.budget = budget
40
+ self.count = count
41
+
42
+ def fit(self, turns: list[Turn], blocks: list[Block], turn_cost: Callable[[Turn], int]) -> tuple[list[Turn], list[Block], ContextReport]:
43
+ blocks = [Block(b.source, b.text, b.tokens or self.count(b.text)) for b in blocks]
44
+ turns = list(turns)
45
+ used = sum(turn_cost(t) for t in turns) + sum(b.tokens for b in blocks)
46
+ dropped: list[str] = []
47
+ lost = 0
48
+
49
+ def drop_block(source: str) -> None:
50
+ nonlocal used
51
+ for b in [b for b in blocks if b.source == source]:
52
+ blocks.remove(b)
53
+ used -= b.tokens
54
+ dropped.append(source)
55
+
56
+ def drop_turns(keep: int) -> None:
57
+ nonlocal used, lost
58
+ while used > self.budget and len(turns) > keep:
59
+ cost = turn_cost(turns.pop(0))
60
+ used -= cost
61
+ lost += cost
62
+
63
+ before = len(turns)
64
+ for source in ("memory", "facts"):
65
+ if used > self.budget:
66
+ drop_block(source)
67
+ drop_turns(keep=2)
68
+ for source in ("documents", "graph"):
69
+ if used > self.budget:
70
+ drop_block(source)
71
+ drop_turns(keep=1)
72
+ by_source = {"history": sum(turn_cost(t) for t in turns)}
73
+ for b in blocks:
74
+ by_source[b.source] = by_source.get(b.source, 0) + b.tokens
75
+ return turns, blocks, ContextReport(self.budget, used, by_source, dropped, before - len(turns), lost)
@@ -0,0 +1,45 @@
1
+ import time
2
+ from dataclasses import dataclass
3
+
4
+ from pal.context_manager.utils.hashing import hash_segment
5
+ from pal.context_manager.utils.storage import InMemoryStorage, StorageBackend
6
+
7
+
8
+ @dataclass
9
+ class CacheEntry:
10
+ content: str
11
+ created_at: float
12
+ hits: int = 0
13
+
14
+
15
+ class ContextCache:
16
+ def __init__(self, storage: StorageBackend = None, ttl: float = 3600):
17
+ self.storage = storage or InMemoryStorage()
18
+ self.ttl = ttl
19
+ self.hits = 0
20
+ self.misses = 0
21
+
22
+ def get_or_store(self, content: str) -> tuple[bool, str]:
23
+ key = hash_segment(content)
24
+ entry = self.storage.get(key)
25
+
26
+ if entry and (time.time() - entry.created_at) < self.ttl:
27
+ entry.hits += 1
28
+ self.storage.set(key, entry)
29
+ self.hits += 1
30
+ return True, key
31
+
32
+ self.storage.set(key, CacheEntry(content=content, created_at=time.time()))
33
+ self.misses += 1
34
+ return False, key
35
+
36
+ def resolve(self, key: str) -> str | None:
37
+ entry = self.storage.get(key)
38
+ return entry.content if entry else None
39
+
40
+ def hit_rate(self) -> float:
41
+ total = self.hits + self.misses
42
+ return self.hits / total if total else 0.0
43
+
44
+ def size_bytes(self) -> int:
45
+ return sum(len(self.storage.get(k).content.encode("utf-8")) for k in self.storage.keys())
@@ -0,0 +1,16 @@
1
+ from pal.llm_wrapper.components.base import LLMWrapper
2
+
3
+
4
+ class KVBridge:
5
+ """Delegates dedup to the active LLM core's native KV cache when
6
+ available (e.g. llama.cpp's prompt-prefix cache, Ollama's automatic
7
+ prefix reuse), instead of duplicating that logic here."""
8
+
9
+ def __init__(self, core: LLMWrapper):
10
+ self.core = core
11
+
12
+ def active(self) -> bool:
13
+ return self.core.supports_kv_cache()
14
+
15
+ def reset(self) -> None:
16
+ self.core.reset_kv_cache()
@@ -0,0 +1,48 @@
1
+ import time
2
+ from dataclasses import dataclass, field
3
+
4
+ from pal.context_manager.utils.relevance import recency_score, text_similarity
5
+
6
+
7
+ @dataclass
8
+ class Segment:
9
+ id: str
10
+ content: str
11
+ role: str = "user"
12
+ timestamp: float = field(default_factory=time.time)
13
+
14
+
15
+ class ContextPager:
16
+ def __init__(self, hot_window: int = 6, cold_threshold: float = 0.35):
17
+ self.hot_window = hot_window
18
+ self.cold_threshold = cold_threshold
19
+ self.cold_store: dict[str, Segment] = {}
20
+
21
+ def page_out(self, history: list[Segment]) -> tuple[list[Segment], list[str]]:
22
+ if len(history) <= self.hot_window:
23
+ return history, []
24
+
25
+ hot = history[-self.hot_window:]
26
+ cold = history[: -self.hot_window]
27
+ cold_refs = []
28
+ for seg in cold:
29
+ self.cold_store[seg.id] = seg
30
+ cold_refs.append(seg.id)
31
+ return hot, cold_refs
32
+
33
+ def page_in(self, query: str, cold_refs: list[str], top_k: int = 2) -> list[Segment]:
34
+ candidates = [self.cold_store[r] for r in cold_refs if r in self.cold_store]
35
+ if not candidates:
36
+ return []
37
+
38
+ texts = [c.content for c in candidates]
39
+ sims = text_similarity(query, texts) # embeddings if available, keyword overlap otherwise
40
+ now = time.time()
41
+
42
+ scored = []
43
+ for seg, sim in zip(candidates, sims):
44
+ score = 0.7 * sim + 0.3 * recency_score(seg.timestamp, now)
45
+ scored.append((score, seg))
46
+
47
+ scored.sort(key=lambda x: x[0], reverse=True)
48
+ return [seg for score, seg in scored[:top_k] if score >= self.cold_threshold]
@@ -0,0 +1,56 @@
1
+ """SimpleContextManager (AIOS Appendix A.4): pause a long generation when its time slice ends and resume it later without
2
+ losing the work done.
3
+
4
+ Two ways, chosen by what the core can give us. Text-based (every cloud API and local server): the text decoded so far is the
5
+ snapshot, and resuming continues from it. Logits-based (an in-process Hugging Face model): the beam-search state is the
6
+ snapshot. Both are done by the interrupt controller already used by the Round Robin scheduler; this class gives it the
7
+ per-caller save, load and clear interface the adapter needs.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import threading
12
+ from typing import Any
13
+
14
+ from pal.llm_wrapper.components.base import LLMSyscall, LLMWrapper
15
+
16
+
17
+ class SimpleContextManager:
18
+ def __init__(self) -> None:
19
+ self._contexts: dict[Any, Any] = {}
20
+ self._lock = threading.Lock()
21
+ from pal.orchestrator.components.context_interrupt import (
22
+ ContextInterruptController, # lazy: avoids a cycle
23
+ )
24
+ self._controller = ContextInterruptController()
25
+
26
+ def load_context(self, pid: Any) -> Any:
27
+ with self._lock:
28
+ return self._contexts.get(pid)
29
+
30
+ def clear_context(self, pid: Any) -> None:
31
+ with self._lock:
32
+ self._contexts.pop(pid, None)
33
+
34
+ def generate_response_with_interruption(
35
+ self, model_name: str, model: LLMWrapper, messages: list[dict], tools: list[dict] | None, message_return_type: str,
36
+ temperature: float | None, max_tokens: int | None, pid: Any, time_limit: float, response_format: dict | None = None,
37
+ ) -> tuple[str, bool]:
38
+ """Generate for at most `time_limit` seconds. Returns (text so far, finished). If not finished, the state is kept under
39
+ `pid`; calling again with the same pid continues from it. Plain text only: tool calls and JSON output are not interrupted."""
40
+ from pal.orchestrator.components.context_interrupt import ContextSnapshot, InterruptResult
41
+
42
+ base = LLMSyscall(messages=messages, model=model_name or None, temperature=temperature, max_tokens=max_tokens)
43
+ snap = self.load_context(pid)
44
+ prior = ""
45
+ if snap is not None and snap.mode == "text":
46
+ prior = snap.text_so_far
47
+ partial = InterruptResult(complete=False, text_so_far=prior, elapsed=0.0, snapshot=snap)
48
+ base = self._controller.build_continuation(base, partial)
49
+ res = self._controller.run_slice(model, base, time_limit, resume_snapshot=snap if snap and snap.mode == "logits" else None)
50
+ text = res.text_so_far if res.cumulative else prior + res.text_so_far
51
+ if res.complete:
52
+ self.clear_context(pid)
53
+ return text, True
54
+ with self._lock:
55
+ self._contexts[pid] = ContextSnapshot(mode=res.snapshot.mode, text_so_far=text, beam_state=res.snapshot.beam_state)
56
+ return text, False
@@ -0,0 +1,88 @@
1
+ """Paging for chat history, one turn (user message + reply) at a time.
2
+
3
+ The last `hot_turns` turns are always sent. Older turns are "cold": they are dropped from the
4
+ request unless they look relevant to the current question, in which case up to `recall_turns` of
5
+ them are paged back in (best match first, sent in chronological order). A token cap on the hot
6
+ window trims it further if a few turns are very long.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import time
11
+ from collections.abc import Callable
12
+ from dataclasses import dataclass, field
13
+
14
+ from pal.context_manager.components.pager import ContextPager, Segment
15
+ from pal.tokens import estimate_tokens
16
+
17
+
18
+ @dataclass
19
+ class Turn:
20
+ user: str
21
+ assistant: str
22
+ local: bool = False # answered by the local LLM
23
+ ts: float = field(default_factory=time.time)
24
+ id: str = ""
25
+ files: tuple = () # files an agent saved in this turn (the content lives in the workspace)
26
+ # cached by the compressor, never part of the turn's identity
27
+ _keys: tuple[frozenset, frozenset, list] | None = field(default=None, repr=False, compare=False)
28
+ _facts: tuple[list[str], list[str]] | None = field(default=None, repr=False, compare=False)
29
+
30
+ def text(self) -> str:
31
+ return f"{self.user}\n{self.assistant}"
32
+
33
+
34
+ @dataclass
35
+ class PagedHistory:
36
+ turns: list[Turn] # what to send, chronological
37
+ recalled: int # cold turns paged back in
38
+ full_tokens: int # cost of sending every turn
39
+ sent_tokens: int # cost of what is sent
40
+ dropped_turns: int
41
+
42
+ @property
43
+ def saved_tokens(self) -> int:
44
+ return max(0, self.full_tokens - self.sent_tokens)
45
+
46
+
47
+ class TurnPager:
48
+ def __init__(self, hot_turns: int = 6, recall_turns: int = 2, recall_threshold: float = 0.35,
49
+ max_history_tokens: int = 6000, count: Callable[[str], int] = estimate_tokens):
50
+ self.hot_turns = hot_turns
51
+ self.recall_turns = recall_turns
52
+ self.recall_threshold = recall_threshold
53
+ self.max_history_tokens = max_history_tokens
54
+ self.count = count
55
+
56
+ def _cost(self, turn: Turn) -> int:
57
+ return self.count(turn.user) + self.count(turn.assistant) + 8
58
+
59
+ turn_cost = _cost # public name for the assembler
60
+
61
+ def select(self, turns: list[Turn], query: str) -> PagedHistory:
62
+ full = sum(self._cost(t) for t in turns)
63
+ if len(turns) <= 1 and full <= self.max_history_tokens:
64
+ return PagedHistory(list(turns), 0, full, full, 0)
65
+
66
+ index = {str(i): t for i, t in enumerate(turns)}
67
+ segments = [Segment(id=str(i), content=t.text(), role="turn", timestamp=t.ts) for i, t in enumerate(turns)]
68
+ pager = ContextPager(hot_window=self.hot_turns, cold_threshold=self.recall_threshold)
69
+ hot_segs, cold_refs = pager.page_out(segments)
70
+ hot = [index[s.id] for s in hot_segs]
71
+
72
+ # token cap: push the oldest hot turns to cold while over budget (always keep the newest)
73
+ while len(hot) > 1 and sum(self._cost(t) for t in hot) > self.max_history_tokens:
74
+ oldest = hot.pop(0)
75
+ cold_refs.append(next(k for k, v in index.items() if v is oldest))
76
+
77
+ recalled: list[Turn] = []
78
+ if self.recall_turns and cold_refs and query.strip():
79
+ room = self.max_history_tokens - sum(self._cost(t) for t in hot)
80
+ for seg in pager.page_in(query, cold_refs, top_k=self.recall_turns):
81
+ t = index[seg.id]
82
+ if self._cost(t) <= room:
83
+ recalled.append(t)
84
+ room -= self._cost(t)
85
+ pos = {id(t): i for i, t in enumerate(turns)}
86
+ chosen = sorted(recalled + hot, key=lambda t: pos[id(t)])
87
+ sent = sum(self._cost(t) for t in chosen)
88
+ return PagedHistory(chosen, len(recalled), full, sent, len(turns) - len(chosen))
@@ -0,0 +1,27 @@
1
+ from pal.context_manager.components.context_cache import ContextCache
2
+ from pal.context_manager.components.kv_bridge import KVBridge
3
+ from pal.context_manager.components.pager import ContextPager, Segment
4
+ from pal.llm_wrapper.components.base import LLMWrapper
5
+
6
+
7
+ class ContextManagerPipeline:
8
+ """Ties system-prompt caching, hot/cold history paging, and the
9
+ backend's native KV cache together behind one small interface."""
10
+
11
+ def __init__(self, core: LLMWrapper, cache_ttl: float = 3600, hot_window: int = 6, cold_threshold: float = 0.35):
12
+ self.cache = ContextCache(ttl=cache_ttl)
13
+ self.pager = ContextPager(hot_window=hot_window, cold_threshold=cold_threshold)
14
+ self.kv_bridge = KVBridge(core)
15
+
16
+ def cache_system_prompt(self, system_prompt: str) -> tuple[bool, str]:
17
+ return self.cache.get_or_store(system_prompt)
18
+
19
+ def page_history(self, history: list[Segment], query: str) -> list[Segment]:
20
+ """Returns hot + relevant paged-in cold segments, in order, ready
21
+ to append to a request's message list."""
22
+ hot, cold_refs = self.pager.page_out(history)
23
+ recalled = self.pager.page_in(query, cold_refs)
24
+ return recalled + hot
25
+
26
+ def size_bytes(self) -> int:
27
+ return self.cache.size_bytes()
@@ -0,0 +1,18 @@
1
+ from pal.context_manager.utils.hashing import hash_segment
2
+ from pal.context_manager.utils.relevance import (
3
+ embedding_similarity,
4
+ keyword_overlap,
5
+ recency_score,
6
+ text_similarity,
7
+ )
8
+ from pal.context_manager.utils.storage import InMemoryStorage, StorageBackend
9
+
10
+ __all__ = [
11
+ "hash_segment",
12
+ "embedding_similarity",
13
+ "keyword_overlap",
14
+ "text_similarity",
15
+ "recency_score",
16
+ "InMemoryStorage",
17
+ "StorageBackend",
18
+ ]
@@ -0,0 +1,5 @@
1
+ import hashlib
2
+
3
+
4
+ def hash_segment(content: str) -> str:
5
+ return hashlib.sha256(content.encode("utf-8")).hexdigest()
@@ -0,0 +1,59 @@
1
+ import re
2
+ import time
3
+
4
+ try:
5
+ import numpy as np
6
+ from sentence_transformers import SentenceTransformer
7
+ _EMBEDDER = None
8
+ _EMBED_AVAILABLE = True
9
+ except ImportError:
10
+ _EMBED_AVAILABLE = False
11
+
12
+
13
+ def _get_embedder():
14
+ global _EMBEDDER
15
+ if _EMBEDDER is None:
16
+ _EMBEDDER = SentenceTransformer("all-MiniLM-L6-v2")
17
+ return _EMBEDDER
18
+
19
+
20
+ def embedding_similarity(query: str, segments: list[str]) -> list[float]:
21
+ if not _EMBED_AVAILABLE or not segments:
22
+ return [0.0] * len(segments)
23
+ model = _get_embedder()
24
+ q_vec = model.encode([query])[0]
25
+ s_vecs = model.encode(segments)
26
+ sims = s_vecs @ q_vec / (np.linalg.norm(s_vecs, axis=1) * np.linalg.norm(q_vec) + 1e-8)
27
+ return sims.tolist()
28
+
29
+
30
+ def recency_score(timestamp: float, now: float = None) -> float:
31
+ now = now or time.time()
32
+ age = max(0.0, now - timestamp)
33
+ return 1.0 / (1.0 + age / 60.0)
34
+
35
+
36
+ _WORD = re.compile(r"[^\W_]+", re.UNICODE)
37
+ _STOP = frozenset("a an the and or but if of to in on at for with by from as is are was were be been it this that "
38
+ "these those i you he she we they me my your our their what which who how why when where do does did "
39
+ "can could should would will please".split())
40
+
41
+
42
+ def _keywords(text: str) -> set[str]:
43
+ return {w for w in (m.lower() for m in _WORD.findall(text)) if w not in _STOP and (len(w) > 1 or w.isdigit())}
44
+
45
+
46
+ def keyword_overlap(query: str, text: str) -> float:
47
+ """Share of the query's meaningful words that also appear in `text` (0 to 1). No model needed."""
48
+ q, t = _keywords(query), _keywords(text)
49
+ if not q or not t:
50
+ return 0.0
51
+ return len(q & t) / len(q)
52
+
53
+
54
+ def text_similarity(query: str, texts: list[str]) -> list[float]:
55
+ """Embedding similarity when sentence-transformers is installed, keyword overlap otherwise."""
56
+ sims = embedding_similarity(query, texts)
57
+ if texts and all(s == 0.0 for s in sims):
58
+ sims = [keyword_overlap(query, t) for t in texts]
59
+ return sims
@@ -0,0 +1,37 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, Optional
3
+
4
+
5
+ class StorageBackend(ABC):
6
+ @abstractmethod
7
+ def get(self, key: str) -> Optional[Any]:
8
+ ...
9
+
10
+ @abstractmethod
11
+ def set(self, key: str, value: Any) -> None:
12
+ ...
13
+
14
+ @abstractmethod
15
+ def delete(self, key: str) -> None:
16
+ ...
17
+
18
+ @abstractmethod
19
+ def keys(self) -> list[str]:
20
+ ...
21
+
22
+
23
+ class InMemoryStorage(StorageBackend):
24
+ def __init__(self):
25
+ self._store: dict[str, Any] = {}
26
+
27
+ def get(self, key):
28
+ return self._store.get(key)
29
+
30
+ def set(self, key, value):
31
+ self._store[key] = value
32
+
33
+ def delete(self, key):
34
+ self._store.pop(key, None)
35
+
36
+ def keys(self):
37
+ return list(self._store.keys())
@@ -0,0 +1,4 @@
1
+ from pal.documents.components.chunker import Chunk, chunk_markdown
2
+ from pal.documents.components.index import DocInfo, DocumentIndex, Hit
3
+
4
+ __all__ = ["Chunk", "DocInfo", "DocumentIndex", "Hit", "chunk_markdown"]
File without changes
@@ -0,0 +1,79 @@
1
+ """Split markdown into chunks of about `target_tokens`, at heading and paragraph boundaries.
2
+ Pure and deterministic: the same text always gives the same chunks, so part numbers are stable."""
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Callable
7
+ from dataclasses import dataclass
8
+
9
+ from pal.tokens import estimate_tokens
10
+
11
+ _HEADING = re.compile(r"^#{1,6}\s+(.*\S)\s*$")
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class Chunk:
16
+ index: int # 0-based
17
+ heading: str # nearest heading above the chunk ("" if none)
18
+ text: str
19
+ tokens: int
20
+
21
+
22
+ def _split_long(paragraph: str, target: int, count: Callable[[str], int]) -> list[str]:
23
+ """Break one oversized paragraph on line, then sentence, then character boundaries."""
24
+ if count(paragraph) <= target:
25
+ return [paragraph]
26
+ pieces: list[str] = []
27
+ for line in paragraph.split("\n"):
28
+ pieces.extend(re.split(r"(?<=[.!?])\s+", line) if count(line) > target else [line])
29
+ out: list[str] = []
30
+ cur = ""
31
+ for piece in pieces:
32
+ while count(piece) > target: # a single huge "sentence": hard cut
33
+ cut = target * 4
34
+ out.append(piece[:cut])
35
+ piece = piece[cut:]
36
+ if cur and count(cur + "\n" + piece) > target:
37
+ out.append(cur)
38
+ cur = piece
39
+ else:
40
+ cur = f"{cur}\n{piece}" if cur else piece
41
+ if cur:
42
+ out.append(cur)
43
+ return out
44
+
45
+
46
+ def chunk_markdown(text: str, target_tokens: int = 350, count: Callable[[str], int] = estimate_tokens) -> list[Chunk]:
47
+ text = text.replace("\r\n", "\n").strip()
48
+ if not text:
49
+ return []
50
+ chunks: list[Chunk] = []
51
+ heading = ""
52
+ buf: list[str] = []
53
+ buf_heading = ""
54
+
55
+ def flush() -> None:
56
+ nonlocal buf
57
+ if buf:
58
+ body = "\n\n".join(buf).strip()
59
+ if body:
60
+ chunks.append(Chunk(len(chunks), buf_heading, body, count(body)))
61
+ buf = []
62
+
63
+ for para in re.split(r"\n\s*\n", text):
64
+ para = para.strip("\n")
65
+ if not para.strip():
66
+ continue
67
+ m = _HEADING.match(para.split("\n", 1)[0])
68
+ if m:
69
+ flush() # a heading starts a new chunk
70
+ heading = m.group(1)
71
+ for piece in _split_long(para, target_tokens, count):
72
+ heading_only = len(buf) == 1 and _HEADING.match(buf[0]) is not None
73
+ if buf and not heading_only and count("\n\n".join(buf + [piece])) > target_tokens:
74
+ flush()
75
+ if not buf:
76
+ buf_heading = heading
77
+ buf.append(piece)
78
+ flush()
79
+ return chunks