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,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,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())
|
|
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
|