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,215 @@
|
|
|
1
|
+
"""The local model's scheduler: preemptive priority, first come first served inside each class.
|
|
2
|
+
|
|
3
|
+
One job runs at a time. Classes in order: AGENT, then CHAT, then OTHER. When a job of a better class is waiting, the
|
|
4
|
+
running job is paused at its next safe point and the better job goes first; the paused job then resumes before any later
|
|
5
|
+
arrival of its own class, so first come first served is kept inside a class.
|
|
6
|
+
|
|
7
|
+
Three kinds of job, because a model call cannot always be paused:
|
|
8
|
+
* atomic one call that finishes (route, trim, verify: structured and short). Never paused.
|
|
9
|
+
* steps a generator of small pieces (summarise a section, tidy a note). Paused between pieces: no state to save.
|
|
10
|
+
* stream a plain-text generation. Paused between chunks: the stream is closed (so the model stops), the text so far is
|
|
11
|
+
kept, and the job resumes by continuing from that text. Only for cores that really stream.
|
|
12
|
+
A job can be paused at most `max_pauses` times, then it runs to the end, so it cannot be postponed forever.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import threading
|
|
18
|
+
import time
|
|
19
|
+
from collections import deque
|
|
20
|
+
from collections.abc import Callable, Iterator
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from pal.llm_scheduler.types import ExpiredError, QueueFullError, SchedulerError, TaskClass, Ticket
|
|
24
|
+
from pal.llm_wrapper.components.base import LLMSyscall, LLMWrapper
|
|
25
|
+
|
|
26
|
+
log = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _Job:
|
|
30
|
+
preemptible = False
|
|
31
|
+
|
|
32
|
+
def __init__(self, ticket: Ticket, cls: TaskClass, deadline: float | None = None):
|
|
33
|
+
self.ticket, self.cls, self.deadline = ticket, cls, deadline
|
|
34
|
+
self.result: Any = None
|
|
35
|
+
|
|
36
|
+
def run(self, should_stop: Callable[[], bool]) -> bool: # True when finished
|
|
37
|
+
raise NotImplementedError
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class _AtomicJob(_Job):
|
|
41
|
+
def __init__(self, ticket: Ticket, cls: TaskClass, fn: Callable[[], Any], deadline: float | None = None):
|
|
42
|
+
super().__init__(ticket, cls, deadline)
|
|
43
|
+
self.fn = fn
|
|
44
|
+
|
|
45
|
+
def run(self, should_stop: Callable[[], bool]) -> bool:
|
|
46
|
+
self.result = self.fn()
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class _StepsJob(_Job):
|
|
51
|
+
preemptible = True
|
|
52
|
+
|
|
53
|
+
def __init__(self, ticket: Ticket, cls: TaskClass, steps: Iterator[Any], deadline: float | None = None):
|
|
54
|
+
super().__init__(ticket, cls, deadline)
|
|
55
|
+
self.steps = steps
|
|
56
|
+
|
|
57
|
+
def run(self, should_stop: Callable[[], bool]) -> bool:
|
|
58
|
+
while True:
|
|
59
|
+
try:
|
|
60
|
+
next(self.steps)
|
|
61
|
+
except StopIteration as done:
|
|
62
|
+
self.result = done.value
|
|
63
|
+
return True
|
|
64
|
+
if should_stop():
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class _StreamJob(_Job):
|
|
69
|
+
preemptible = True
|
|
70
|
+
|
|
71
|
+
def __init__(self, ticket: Ticket, cls: TaskClass, core: LLMWrapper, syscall: LLMSyscall):
|
|
72
|
+
super().__init__(ticket, cls)
|
|
73
|
+
self.core, self.syscall, self.text = core, syscall, ""
|
|
74
|
+
|
|
75
|
+
def _continuation(self) -> LLMSyscall:
|
|
76
|
+
from pal.orchestrator.components.context_interrupt import ( # lazy: avoids an import cycle
|
|
77
|
+
ContextInterruptController,
|
|
78
|
+
ContextSnapshot,
|
|
79
|
+
InterruptResult,
|
|
80
|
+
)
|
|
81
|
+
partial = InterruptResult(complete=False, text_so_far=self.text, elapsed=0.0,
|
|
82
|
+
snapshot=ContextSnapshot(mode="text", text_so_far=self.text))
|
|
83
|
+
return ContextInterruptController().build_continuation(self.syscall, partial)
|
|
84
|
+
|
|
85
|
+
def run(self, should_stop: Callable[[], bool]) -> bool:
|
|
86
|
+
stream = iter(self.core.stream_chat(self.syscall if not self.text else self._continuation()))
|
|
87
|
+
try:
|
|
88
|
+
chunk = next(stream, None)
|
|
89
|
+
while chunk is not None:
|
|
90
|
+
self.text += chunk
|
|
91
|
+
chunk = next(stream, None) # look at the next chunk before deciding to stop,
|
|
92
|
+
if chunk is None: # so a stream that has just ended is never paused
|
|
93
|
+
break
|
|
94
|
+
if should_stop():
|
|
95
|
+
self.text += chunk
|
|
96
|
+
return False
|
|
97
|
+
finally:
|
|
98
|
+
close = getattr(stream, "close", None)
|
|
99
|
+
if close:
|
|
100
|
+
close() # closing the stream makes the model stop generating
|
|
101
|
+
self.result = self.text
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class LocalScheduler:
|
|
106
|
+
def __init__(self, max_pauses: int = 2, other_stale_seconds: float = 600.0, max_queue: int = 200):
|
|
107
|
+
self.max_pauses = max_pauses
|
|
108
|
+
self.other_stale_seconds = other_stale_seconds
|
|
109
|
+
self.max_queue = max_queue
|
|
110
|
+
self._queues: dict[TaskClass, deque[_Job]] = {c: deque() for c in TaskClass}
|
|
111
|
+
self._cond = threading.Condition()
|
|
112
|
+
self._closed = False
|
|
113
|
+
self.history: deque[dict] = deque(maxlen=500)
|
|
114
|
+
self._thread = threading.Thread(target=self._worker, name="pal-local-scheduler", daemon=True)
|
|
115
|
+
self._thread.start()
|
|
116
|
+
|
|
117
|
+
# -- submitting -------------------------------------------------------------------------------------------
|
|
118
|
+
def submit(self, fn: Callable[[], Any], task_class: TaskClass = TaskClass.CHAT, label: str = "local job") -> Ticket:
|
|
119
|
+
"""An atomic job: runs to the end, never paused."""
|
|
120
|
+
t = Ticket(label, task_class)
|
|
121
|
+
return self._enqueue(_AtomicJob(t, task_class, fn, self._deadline(task_class, None)))
|
|
122
|
+
|
|
123
|
+
def submit_steps(self, steps: Iterator[Any], task_class: TaskClass = TaskClass.OTHER, label: str = "local steps",
|
|
124
|
+
expires_after: float | None = None) -> Ticket:
|
|
125
|
+
"""A job made of small pieces: `steps` yields after each piece; its return value is the result."""
|
|
126
|
+
t = Ticket(label, task_class)
|
|
127
|
+
return self._enqueue(_StepsJob(t, task_class, steps, self._deadline(task_class, expires_after)))
|
|
128
|
+
|
|
129
|
+
def submit_stream(self, core: LLMWrapper, syscall: LLMSyscall, task_class: TaskClass = TaskClass.CHAT,
|
|
130
|
+
label: str = "local answer") -> Ticket:
|
|
131
|
+
"""A plain-text generation that can be paused and resumed. The result is the whole text."""
|
|
132
|
+
t = Ticket(label, task_class)
|
|
133
|
+
return self._enqueue(_StreamJob(t, task_class, core, syscall))
|
|
134
|
+
|
|
135
|
+
def _deadline(self, cls: TaskClass, expires_after: float | None) -> float | None:
|
|
136
|
+
if expires_after is None and cls == TaskClass.OTHER:
|
|
137
|
+
expires_after = self.other_stale_seconds
|
|
138
|
+
return time.monotonic() + expires_after if expires_after else None
|
|
139
|
+
|
|
140
|
+
def _enqueue(self, job: _Job) -> Ticket:
|
|
141
|
+
with self._cond:
|
|
142
|
+
if self._closed:
|
|
143
|
+
raise SchedulerError("the scheduler is closed")
|
|
144
|
+
if sum(len(q) for q in self._queues.values()) >= self.max_queue:
|
|
145
|
+
raise QueueFullError(f"too many local jobs are waiting ({self.max_queue})")
|
|
146
|
+
self._queues[job.cls].append(job)
|
|
147
|
+
self._cond.notify()
|
|
148
|
+
return job.ticket
|
|
149
|
+
|
|
150
|
+
def pending(self) -> dict[str, int]:
|
|
151
|
+
with self._cond:
|
|
152
|
+
return {c.name.lower(): len(q) for c, q in self._queues.items()}
|
|
153
|
+
|
|
154
|
+
# -- the worker ----------------------------------------------------------------------------------------------
|
|
155
|
+
def _higher_waiting(self, cls: TaskClass) -> bool:
|
|
156
|
+
with self._cond:
|
|
157
|
+
return any(self._queues[c] for c in TaskClass if c < cls)
|
|
158
|
+
|
|
159
|
+
def _next_locked(self) -> _Job | None:
|
|
160
|
+
for cls in TaskClass:
|
|
161
|
+
q = self._queues[cls]
|
|
162
|
+
while q:
|
|
163
|
+
job = q.popleft()
|
|
164
|
+
if job.deadline is not None and time.monotonic() > job.deadline and job.ticket.started is None:
|
|
165
|
+
job.ticket.set_error(ExpiredError(f"{job.ticket.label} waited too long and was dropped"))
|
|
166
|
+
continue
|
|
167
|
+
return job
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
def _worker(self) -> None:
|
|
171
|
+
while True:
|
|
172
|
+
with self._cond:
|
|
173
|
+
job = self._next_locked()
|
|
174
|
+
while job is None:
|
|
175
|
+
if self._closed:
|
|
176
|
+
return
|
|
177
|
+
self._cond.wait(timeout=1.0)
|
|
178
|
+
job = self._next_locked()
|
|
179
|
+
self._run(job)
|
|
180
|
+
|
|
181
|
+
def _run(self, job: _Job) -> None:
|
|
182
|
+
t = job.ticket
|
|
183
|
+
if t.started is None:
|
|
184
|
+
t.started = time.monotonic()
|
|
185
|
+
began = time.monotonic()
|
|
186
|
+
|
|
187
|
+
def should_stop() -> bool:
|
|
188
|
+
return job.preemptible and t.pauses < self.max_pauses and self._higher_waiting(job.cls)
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
finished = job.run(should_stop)
|
|
192
|
+
except BaseException as e: # noqa: BLE001 (the error belongs to whoever submitted the job)
|
|
193
|
+
t.run_seconds += time.monotonic() - began
|
|
194
|
+
t.set_error(e)
|
|
195
|
+
return
|
|
196
|
+
t.run_seconds += time.monotonic() - began
|
|
197
|
+
if finished:
|
|
198
|
+
self.history.append({"label": t.label, "class": job.cls.name, "waited": round(t.waited, 4),
|
|
199
|
+
"ran": round(t.run_seconds, 4), "pauses": t.pauses})
|
|
200
|
+
t.set_result(job.result)
|
|
201
|
+
return
|
|
202
|
+
t.pauses += 1
|
|
203
|
+
log.info("paused %s (%s) for a higher-priority job; pause %d", t.label, job.cls.name, t.pauses)
|
|
204
|
+
with self._cond:
|
|
205
|
+
self._queues[job.cls].appendleft(job) # resumes before later arrivals of its own class
|
|
206
|
+
|
|
207
|
+
def close(self) -> None:
|
|
208
|
+
with self._cond:
|
|
209
|
+
self._closed = True
|
|
210
|
+
dropped = [j for q in self._queues.values() for j in q]
|
|
211
|
+
for q in self._queues.values():
|
|
212
|
+
q.clear()
|
|
213
|
+
self._cond.notify_all()
|
|
214
|
+
for j in dropped:
|
|
215
|
+
j.ticket.set_error(SchedulerError("the scheduler was closed"))
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""The scheduler as one object: the local model's preemptive priority scheduler, the API model's first-come-first-served
|
|
2
|
+
lines, and the slots for code work."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pal.config import SchedulerConfig
|
|
6
|
+
from pal.llm_scheduler import core as _core
|
|
7
|
+
from pal.llm_scheduler.api import ApiScheduler
|
|
8
|
+
from pal.llm_scheduler.code import CodeSlots
|
|
9
|
+
from pal.llm_scheduler.core import ScheduledWrapper
|
|
10
|
+
from pal.llm_scheduler.local import LocalScheduler
|
|
11
|
+
from pal.llm_wrapper.components.base import LLMWrapper
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LLMScheduler:
|
|
15
|
+
def __init__(self, cfg: SchedulerConfig | dict | None = None):
|
|
16
|
+
self.cfg = SchedulerConfig.coerce(cfg)
|
|
17
|
+
c = self.cfg
|
|
18
|
+
self.local = LocalScheduler(c.local_max_pauses, c.other_stale_seconds, c.max_queue) if c.enabled else None
|
|
19
|
+
self.api = ApiScheduler(c.api_retry_429, c.api_backoff_seconds, c.max_queue, enabled=c.enabled)
|
|
20
|
+
self.code = CodeSlots(c.code_workers if c.enabled else 0)
|
|
21
|
+
self.task = _core.task
|
|
22
|
+
self.track = _core.track
|
|
23
|
+
|
|
24
|
+
def wrap_local(self, core: LLMWrapper | None) -> LLMWrapper | None:
|
|
25
|
+
if core is None or isinstance(core, ScheduledWrapper):
|
|
26
|
+
return core
|
|
27
|
+
return ScheduledWrapper(core, self.local)
|
|
28
|
+
|
|
29
|
+
def interval_for(self, provider: str, model: str = "") -> float:
|
|
30
|
+
"""Seconds between call starts on this provider's line. OpenRouter free models are limited to about 20 a minute."""
|
|
31
|
+
if provider == "openrouter" and model.endswith(":free"):
|
|
32
|
+
return float(self.cfg.free_model_interval)
|
|
33
|
+
return float(self.cfg.api_min_interval.get(provider, 0.0))
|
|
34
|
+
|
|
35
|
+
def snapshot(self) -> dict:
|
|
36
|
+
return {"enabled": self.cfg.enabled, "local_waiting": self.local.pending() if self.local else {},
|
|
37
|
+
"api_waiting": self.api.pending()}
|
|
38
|
+
|
|
39
|
+
def close(self) -> None:
|
|
40
|
+
if self.local:
|
|
41
|
+
self.local.close()
|
|
42
|
+
self.api.close()
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Shared types for the scheduler: who asked (task class), a handle on a submitted job (ticket), and per-turn statistics."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from enum import IntEnum
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pal.errors import PalError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TaskClass(IntEnum):
|
|
13
|
+
"""Who asked, which decides local-model priority. A lower number is served first."""
|
|
14
|
+
|
|
15
|
+
AGENT = 0 # preparation for a request sent to an agent
|
|
16
|
+
CHAT = 1 # preparation and easy answers for plain chat
|
|
17
|
+
OTHER = 2 # background work nobody is waiting for
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SchedulerError(PalError):
|
|
21
|
+
http_status = 503
|
|
22
|
+
public = True
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class QueueFullError(SchedulerError):
|
|
26
|
+
"""Too many jobs are already waiting."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ExpiredError(SchedulerError):
|
|
30
|
+
"""A background job waited too long and was dropped."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Ticket:
|
|
34
|
+
"""Returned by every submit. `result()` waits for the job; the other fields say how it went."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, label: str, task_class: TaskClass | None = None):
|
|
37
|
+
self.label = label
|
|
38
|
+
self.task_class = task_class
|
|
39
|
+
self.submitted = time.monotonic()
|
|
40
|
+
self.started: float | None = None # first time the job began to run
|
|
41
|
+
self.run_seconds = 0.0 # time actually running (all slices added up)
|
|
42
|
+
self.pauses = 0 # times a higher-priority job paused it
|
|
43
|
+
self._done = threading.Event()
|
|
44
|
+
self._result: Any = None
|
|
45
|
+
self._error: BaseException | None = None
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def waited(self) -> float:
|
|
49
|
+
"""Seconds between submitting and first starting (what the user feels as queueing)."""
|
|
50
|
+
return (self.started - self.submitted) if self.started is not None else time.monotonic() - self.submitted
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def done(self) -> bool:
|
|
54
|
+
return self._done.is_set()
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def error(self) -> BaseException | None:
|
|
58
|
+
return self._error
|
|
59
|
+
|
|
60
|
+
def set_result(self, value: Any) -> None:
|
|
61
|
+
self._result = value
|
|
62
|
+
self._done.set()
|
|
63
|
+
|
|
64
|
+
def set_error(self, error: BaseException) -> None:
|
|
65
|
+
self._error = error
|
|
66
|
+
self._done.set()
|
|
67
|
+
|
|
68
|
+
def result(self, timeout: float | None = None) -> Any:
|
|
69
|
+
if not self._done.wait(timeout):
|
|
70
|
+
raise TimeoutError(f"{self.label} did not finish within {timeout} s")
|
|
71
|
+
if self._error is not None:
|
|
72
|
+
raise self._error
|
|
73
|
+
return self._result
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TurnStats:
|
|
77
|
+
"""Queueing numbers for one chat turn, filled in by the scheduler as the turn's jobs finish."""
|
|
78
|
+
|
|
79
|
+
def __init__(self) -> None:
|
|
80
|
+
self._lock = threading.Lock()
|
|
81
|
+
self.wait_local = 0.0
|
|
82
|
+
self.wait_api = 0.0
|
|
83
|
+
self.local_pauses = 0
|
|
84
|
+
|
|
85
|
+
def add_local(self, t: Ticket) -> None:
|
|
86
|
+
with self._lock:
|
|
87
|
+
self.wait_local += t.waited
|
|
88
|
+
self.local_pauses += t.pauses
|
|
89
|
+
|
|
90
|
+
def add_api(self, t: Ticket) -> None:
|
|
91
|
+
with self._lock:
|
|
92
|
+
self.wait_api += t.waited
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from pal.llm_wrapper.adapter import LLMAdapter
|
|
2
|
+
from pal.llm_wrapper.components.adapter_types import (
|
|
3
|
+
LLMAdapterResponse,
|
|
4
|
+
LLMAdapterSyscall,
|
|
5
|
+
LLMConfig,
|
|
6
|
+
RouterStrategy,
|
|
7
|
+
)
|
|
8
|
+
from pal.llm_wrapper.components.base import (
|
|
9
|
+
DeploymentType,
|
|
10
|
+
LLMResponse,
|
|
11
|
+
LLMSyscall,
|
|
12
|
+
LLMWrapper,
|
|
13
|
+
WrapperCapabilities,
|
|
14
|
+
WrapperConfig,
|
|
15
|
+
)
|
|
16
|
+
from pal.llm_wrapper.pipeline import LLMWrapperPipeline
|
|
17
|
+
from pal.llm_wrapper.utils.registry import available_backends, build_wrapper
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"WrapperCapabilities",
|
|
21
|
+
"WrapperConfig",
|
|
22
|
+
"DeploymentType",
|
|
23
|
+
"LLMAdapter",
|
|
24
|
+
"LLMAdapterResponse",
|
|
25
|
+
"LLMAdapterSyscall",
|
|
26
|
+
"LLMConfig",
|
|
27
|
+
"LLMWrapper",
|
|
28
|
+
"LLMWrapperPipeline",
|
|
29
|
+
"LLMResponse",
|
|
30
|
+
"LLMSyscall",
|
|
31
|
+
"RouterStrategy",
|
|
32
|
+
"available_backends",
|
|
33
|
+
"build_wrapper",
|
|
34
|
+
]
|