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 Knowledge Graph component: send the relevant slice of a project, not the project.
|
|
2
|
+
|
|
3
|
+
A zip of source code (or one large code file) attached to the chat is indexed once,
|
|
4
|
+
locally, with graphify (AST only: no LLM call, nothing leaves the machine). After that
|
|
5
|
+
each API request carries only the graph nodes and code excerpts that match the current
|
|
6
|
+
question, within `budget_tokens`. The excerpts are added to the request but not stored
|
|
7
|
+
in the chat history, so they are not resent on every later turn.
|
|
8
|
+
|
|
9
|
+
Only source code is indexed. graphify's semantic extraction for docs and images needs
|
|
10
|
+
an LLM and is not used here.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Callable, Optional
|
|
18
|
+
|
|
19
|
+
from pal.config import KnowledgeGraphConfig
|
|
20
|
+
from pal.knowledge_graph.components import builder, retriever, workspace
|
|
21
|
+
from pal.knowledge_graph.components.base import (
|
|
22
|
+
GRAPH_NOTICE,
|
|
23
|
+
GraphError,
|
|
24
|
+
GraphExcerpts,
|
|
25
|
+
ProjectInfo,
|
|
26
|
+
Runner,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__ = ["KnowledgeGraph", "GraphBlock", "GraphError", "GraphExcerpts", "ProjectInfo", "GRAPH_NOTICE"]
|
|
30
|
+
|
|
31
|
+
log = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class GraphBlock:
|
|
36
|
+
"""What the knowledge graph contributes to one request."""
|
|
37
|
+
text: str # the excerpts, ready to add to the request
|
|
38
|
+
saved_tokens: int # what sending the whole project would have cost, minus these excerpts
|
|
39
|
+
info: dict # nodes, excerpts, tokens and project tokens, for the turn's report
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class KnowledgeGraph:
|
|
43
|
+
def __init__(self, cfg=None, runner: Optional[Runner] = None):
|
|
44
|
+
self.cfg = KnowledgeGraphConfig.coerce(cfg)
|
|
45
|
+
self.runner: Runner = runner or builder.default_runner
|
|
46
|
+
self.root = self.cfg.resolved_dir()
|
|
47
|
+
|
|
48
|
+
# -- config ------------------------------------------------------------------------
|
|
49
|
+
@property
|
|
50
|
+
def enabled(self) -> bool:
|
|
51
|
+
return self.cfg.enabled
|
|
52
|
+
|
|
53
|
+
def _ws(self, session_id: str) -> workspace.Workspace:
|
|
54
|
+
return workspace.Workspace(self.root, session_id)
|
|
55
|
+
|
|
56
|
+
def available(self) -> bool:
|
|
57
|
+
return self.runner is not builder.default_runner or builder.graphify_available()
|
|
58
|
+
|
|
59
|
+
def wants(self, name: str, raw: bytes) -> bool:
|
|
60
|
+
"""Should this upload be indexed instead of pasted into the chat?"""
|
|
61
|
+
if not self.enabled:
|
|
62
|
+
return False
|
|
63
|
+
if workspace.ext(name) == "zip":
|
|
64
|
+
return True # a zip can only be used as a project; fails clearly if graphify is missing
|
|
65
|
+
return (self.available() and workspace.is_code(name)
|
|
66
|
+
and len(raw) > self.cfg.min_code_file_chars)
|
|
67
|
+
|
|
68
|
+
# -- indexing --------------------------------------------------------------------------
|
|
69
|
+
def ingest(self, session_id: str, name: str, raw: bytes) -> dict:
|
|
70
|
+
"""Index an upload; returns the attachment dict the chat should show the API model."""
|
|
71
|
+
if not self.available():
|
|
72
|
+
raise GraphError(f"{name}: indexing code needs graphify (pip install graphifyy)")
|
|
73
|
+
ws = self._ws(session_id)
|
|
74
|
+
try:
|
|
75
|
+
if workspace.ext(name) == "zip":
|
|
76
|
+
ws.add_zip(raw, self.cfg.max_zip_files, self.cfg.max_zip_mb * 1_000_000)
|
|
77
|
+
else:
|
|
78
|
+
ws.add_file(Path(name).name, raw)
|
|
79
|
+
info = builder.build(ws, Path(name).stem if workspace.ext(name) == "zip" else Path(name).name, self.runner)
|
|
80
|
+
except GraphError as e:
|
|
81
|
+
raise GraphError(f"{name}: {e}") from e
|
|
82
|
+
return {"name": name, "text": info.summary(), "note": "indexed as a code graph; source not included",
|
|
83
|
+
"kind": "project"}
|
|
84
|
+
|
|
85
|
+
def stats(self) -> dict:
|
|
86
|
+
"""How many projects are indexed right now and how much disk they take (for the Memory Tracker)."""
|
|
87
|
+
from pal.memory_tracker import dir_bytes
|
|
88
|
+
projects = sum(1 for d in self.root.iterdir() if d.is_dir()) if self.root.is_dir() else 0
|
|
89
|
+
return {"enabled": self.enabled, "projects": projects, "bytes": dir_bytes(self.root) if projects else 0}
|
|
90
|
+
|
|
91
|
+
def has_project(self, session_id: str) -> bool:
|
|
92
|
+
return self.enabled and self._ws(session_id).exists()
|
|
93
|
+
|
|
94
|
+
# -- per-question ------------------------------------------------------------------------
|
|
95
|
+
def retrieve(self, session_id: str, question: str,
|
|
96
|
+
count_tokens: Callable[[str], int]) -> Optional[GraphExcerpts]:
|
|
97
|
+
if not self.has_project(session_id) or not question.strip():
|
|
98
|
+
return None
|
|
99
|
+
return retriever.retrieve(self._ws(session_id), question, self.runner, count_tokens,
|
|
100
|
+
self.cfg.budget_tokens, self.cfg.max_slice_lines)
|
|
101
|
+
|
|
102
|
+
def context_block(self, session_id: str, question: str, count_tokens: Callable[[str], int]) -> Optional[GraphBlock]:
|
|
103
|
+
"""The excerpts of the loaded project that match the question, as a block for the request, with the tokens saved against sending the whole
|
|
104
|
+
project. None when there is no project or nothing matches. A failure is logged and gives None: the chat never fails because the graph did."""
|
|
105
|
+
try:
|
|
106
|
+
ex = self.retrieve(session_id, question, count_tokens)
|
|
107
|
+
except Exception: # noqa: BLE001
|
|
108
|
+
log.exception("graph retrieval failed")
|
|
109
|
+
return None
|
|
110
|
+
if not ex:
|
|
111
|
+
return None
|
|
112
|
+
return GraphBlock(ex.text, max(0, ex.project_tokens - ex.tokens),
|
|
113
|
+
{"nodes": ex.nodes, "excerpts": ex.excerpts, "tokens": ex.tokens, "project_tokens": ex.project_tokens})
|
|
114
|
+
|
|
115
|
+
def read_source(self, session_id: str, path: str, start: int = 1, end: int = 0, max_lines: int = 120) -> str:
|
|
116
|
+
"""Read-only view of one indexed source file (used by the read_source tool)."""
|
|
117
|
+
from pal.errors import NotFoundError
|
|
118
|
+
from pal.paths import is_safe_relative_key
|
|
119
|
+
ws = self._ws(session_id)
|
|
120
|
+
rel = path.strip().replace("\\", "/").removeprefix("src/")
|
|
121
|
+
if not ws.exists() or not is_safe_relative_key(rel):
|
|
122
|
+
raise NotFoundError(f"no such file in the project: {path!r}")
|
|
123
|
+
target = (ws.src / rel).resolve()
|
|
124
|
+
if ws.src.resolve() not in target.parents or not target.is_file() or not workspace.is_code(target.name):
|
|
125
|
+
raise NotFoundError(f"no such file in the project: {path!r}")
|
|
126
|
+
lines = target.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
127
|
+
start = max(1, start)
|
|
128
|
+
end = min(len(lines), end if end >= start else start + max_lines - 1, start + max_lines - 1)
|
|
129
|
+
body = "\n".join(f"{i}: {lines[i - 1]}" for i in range(start, end + 1))
|
|
130
|
+
return f"[{rel} lines {start}-{end} of {len(lines)}]\n{body}"
|
|
131
|
+
|
|
132
|
+
# -- read-only code navigation, no model involved ---------------------------------------------------------------------
|
|
133
|
+
def _code_files(self, session_id: str) -> list:
|
|
134
|
+
ws = self._ws(session_id)
|
|
135
|
+
if not ws.exists():
|
|
136
|
+
return []
|
|
137
|
+
return sorted(p for p in ws.src.rglob("*") if p.is_file() and workspace.is_code(p.name))
|
|
138
|
+
|
|
139
|
+
def list_files(self, session_id: str, prefix: str = "", limit: int = 200) -> str:
|
|
140
|
+
"""The project's source files, one per line with the line count."""
|
|
141
|
+
ws = self._ws(session_id)
|
|
142
|
+
rows = []
|
|
143
|
+
for p in self._code_files(session_id):
|
|
144
|
+
rel = p.relative_to(ws.src).as_posix()
|
|
145
|
+
if rel.startswith(prefix.strip().lstrip("/")):
|
|
146
|
+
rows.append(f"{rel} ({sum(1 for _ in p.open(encoding='utf-8', errors='replace'))} lines)")
|
|
147
|
+
if not rows:
|
|
148
|
+
return "no files match" if prefix else "no project"
|
|
149
|
+
more = f"\n[{len(rows) - limit} more not shown: use a prefix]" if len(rows) > limit else ""
|
|
150
|
+
return "\n".join(rows[:limit]) + more
|
|
151
|
+
|
|
152
|
+
def search(self, session_id: str, text: str, limit: int = 25) -> str:
|
|
153
|
+
"""Lines containing `text` (case-insensitive), as path:line: text."""
|
|
154
|
+
needle = text.strip().lower()
|
|
155
|
+
if len(needle) < 2:
|
|
156
|
+
return "give at least two characters to search for"
|
|
157
|
+
return self._grep(session_id, lambda line: needle in line.lower(), limit, f"search for {text.strip()!r}")
|
|
158
|
+
|
|
159
|
+
def find_usages(self, session_id: str, symbol: str, limit: int = 25) -> str:
|
|
160
|
+
"""Where a function, class or variable is defined and where its name is used (a word-boundary text match)."""
|
|
161
|
+
import re
|
|
162
|
+
sym = symbol.strip()
|
|
163
|
+
if not re.fullmatch(r"[A-Za-z_$][\w$.]*", sym):
|
|
164
|
+
return "give a plain name: letters, digits, underscore"
|
|
165
|
+
word = re.compile(rf"(?<![\w$]){re.escape(sym.split('.')[-1])}(?![\w$])")
|
|
166
|
+
define = re.compile(rf"^\s*(?:export\s+|async\s+|public\s+|private\s+|static\s+)*(?:def|class|function|func|fn|interface|struct|enum|trait|type)\s+"
|
|
167
|
+
rf"{re.escape(sym.split('.')[-1])}\b")
|
|
168
|
+
defs, uses = [], []
|
|
169
|
+
for p in self._code_files(session_id):
|
|
170
|
+
rel = p.relative_to(self._ws(session_id).src).as_posix()
|
|
171
|
+
for n, line in enumerate(p.read_text(encoding="utf-8", errors="replace").splitlines(), 1):
|
|
172
|
+
if word.search(line):
|
|
173
|
+
(defs if define.match(line) else uses).append(f"{rel}:{n}: {line.strip()[:160]}")
|
|
174
|
+
if not defs and not uses:
|
|
175
|
+
return f"no match for {sym!r} in the project"
|
|
176
|
+
files = len({u.split(':')[0] for u in defs + uses})
|
|
177
|
+
out = [f"{sym}: {len(defs)} definition(s), {len(uses)} other use(s), in {files} file(s)"]
|
|
178
|
+
out += [f"definition: {d}" for d in defs[:5]]
|
|
179
|
+
out += uses[:limit]
|
|
180
|
+
if len(uses) > limit:
|
|
181
|
+
out.append(f"[{len(uses) - limit} more uses not shown]")
|
|
182
|
+
return "\n".join(out)
|
|
183
|
+
|
|
184
|
+
def _grep(self, session_id: str, test, limit: int, what: str) -> str:
|
|
185
|
+
ws = self._ws(session_id)
|
|
186
|
+
hits = []
|
|
187
|
+
for p in self._code_files(session_id):
|
|
188
|
+
rel = p.relative_to(ws.src).as_posix()
|
|
189
|
+
for n, line in enumerate(p.read_text(encoding="utf-8", errors="replace").splitlines(), 1):
|
|
190
|
+
if test(line):
|
|
191
|
+
hits.append(f"{rel}:{n}: {line.strip()[:160]}")
|
|
192
|
+
if not hits:
|
|
193
|
+
return f"no results for {what}"
|
|
194
|
+
extra = f"\n[{len(hits) - limit} more results not shown]" if len(hits) > limit else ""
|
|
195
|
+
return "\n".join(hits[:limit]) + extra
|
|
196
|
+
|
|
197
|
+
def reset(self, session_id: str) -> None:
|
|
198
|
+
self._ws(session_id).reset()
|
|
199
|
+
|
|
200
|
+
def prune_stale(self, max_age_seconds: float) -> int:
|
|
201
|
+
"""Delete indexed projects nobody has touched for `max_age_seconds` (left behind by a crash or a
|
|
202
|
+
request that failed after the upload). Returns how many were removed."""
|
|
203
|
+
import shutil
|
|
204
|
+
import time
|
|
205
|
+
removed = 0
|
|
206
|
+
if not self.root.is_dir():
|
|
207
|
+
return 0
|
|
208
|
+
for d in self.root.iterdir():
|
|
209
|
+
try:
|
|
210
|
+
if d.is_dir() and time.time() - d.stat().st_mtime > max_age_seconds:
|
|
211
|
+
shutil.rmtree(d, ignore_errors=True)
|
|
212
|
+
removed += 1
|
|
213
|
+
except OSError:
|
|
214
|
+
continue
|
|
215
|
+
return removed
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""The Latency Tracker: where does the time of a chat turn go?
|
|
2
|
+
|
|
3
|
+
Each turn is timed in stages, so a slow turn can be explained rather than guessed at:
|
|
4
|
+
|
|
5
|
+
direct a question answered by code (arithmetic, the time), no model
|
|
6
|
+
routing the local model deciding who answers
|
|
7
|
+
local_answer the local model answering
|
|
8
|
+
input_saver the local model trimming what was typed (and the check that it lost nothing)
|
|
9
|
+
context gathering the request: history, notes, document and code excerpts
|
|
10
|
+
api the API model, with its wait in the queue
|
|
11
|
+
tools tool rounds (a tool call and the model call that follows it)
|
|
12
|
+
other everything else (the turn's total minus the above)
|
|
13
|
+
|
|
14
|
+
`stage()` times a block into the turn's record; `finish()` adds the stages that were already measured elsewhere and the remainder;
|
|
15
|
+
`summarize()` turns a list of turns into per-route latency, queue waits, pauses, and each stage's average, 95th percentile and share of
|
|
16
|
+
all the time spent.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import time
|
|
21
|
+
from collections.abc import Iterable
|
|
22
|
+
from contextlib import contextmanager
|
|
23
|
+
|
|
24
|
+
STAGES = ("direct", "routing", "local_answer", "input_saver", "context", "api", "tools", "other")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def percentile(values: list[float], p: float) -> float:
|
|
28
|
+
if not values:
|
|
29
|
+
return 0.0
|
|
30
|
+
v = sorted(values)
|
|
31
|
+
return v[min(len(v) - 1, int(round(p * (len(v) - 1))))]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class LatencyTracker:
|
|
35
|
+
@staticmethod
|
|
36
|
+
@contextmanager
|
|
37
|
+
def stage(turn, name: str):
|
|
38
|
+
"""Time the block and add it to the turn's `stage_seconds[name]` (a stage can be entered more than once in a turn)."""
|
|
39
|
+
if name not in STAGES:
|
|
40
|
+
raise KeyError(f"unknown stage {name!r} (known: {', '.join(STAGES)})")
|
|
41
|
+
start = time.perf_counter()
|
|
42
|
+
try:
|
|
43
|
+
yield
|
|
44
|
+
finally:
|
|
45
|
+
turn.stage_seconds[name] = turn.stage_seconds.get(name, 0.0) + (time.perf_counter() - start)
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def finish(turn) -> None:
|
|
49
|
+
"""Close the turn's record: the API and tool times measured elsewhere, and `other` = the total minus every named stage."""
|
|
50
|
+
st = turn.stage_seconds
|
|
51
|
+
if "api" not in st and turn.latency_api:
|
|
52
|
+
st["api"] = turn.latency_api
|
|
53
|
+
if "tools" not in st and turn.latency_tools:
|
|
54
|
+
st["tools"] = turn.latency_tools
|
|
55
|
+
named = sum(v for k, v in st.items() if k != "other")
|
|
56
|
+
st["other"] = max(0.0, turn.latency_total - named)
|
|
57
|
+
for k in list(st):
|
|
58
|
+
st[k] = round(st[k], 4)
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def summarize(turns: Iterable) -> dict:
|
|
62
|
+
turns = list(turns)
|
|
63
|
+
n = len(turns)
|
|
64
|
+
by_route: dict = {}
|
|
65
|
+
for r in ("api", "local", "tool"):
|
|
66
|
+
lat = [t.latency_total for t in turns if t.route == r]
|
|
67
|
+
by_route[r] = {"turns": len(lat), "avg_latency_sec": round(sum(lat) / len(lat), 3) if lat else 0.0,
|
|
68
|
+
"p50_latency_sec": round(percentile(lat, 0.5), 3), "p95_latency_sec": round(percentile(lat, 0.95), 3)}
|
|
69
|
+
api_turns = [t for t in turns if t.route == "api"]
|
|
70
|
+
total_time = sum(t.latency_total for t in turns)
|
|
71
|
+
stages: dict = {}
|
|
72
|
+
for name in STAGES:
|
|
73
|
+
vals = [t.stage_seconds[name] for t in turns if t.stage_seconds.get(name, 0) > 0]
|
|
74
|
+
if vals:
|
|
75
|
+
stages[name] = {"turns": len(vals), "mean_sec": round(sum(vals) / len(vals), 3), "p95_sec": round(percentile(vals, 0.95), 3),
|
|
76
|
+
"total_sec": round(sum(vals), 3), "share_pct": round(100.0 * sum(vals) / total_time, 1) if total_time else 0.0}
|
|
77
|
+
slowest = max(stages, key=lambda k: stages[k]["total_sec"]) if stages else ""
|
|
78
|
+
return {
|
|
79
|
+
"by_route": by_route,
|
|
80
|
+
"avg_local_overhead_sec_on_api_turns": round(sum(t.latency_local for t in api_turns) / len(api_turns), 3) if api_turns else 0.0,
|
|
81
|
+
"avg_wait_local_sec": round(sum(t.wait_local for t in turns) / n, 3) if n else 0.0,
|
|
82
|
+
"avg_wait_api_sec": round(sum(t.wait_api for t in api_turns) / len(api_turns), 3) if api_turns else 0.0,
|
|
83
|
+
"local_pauses": sum(t.local_pauses for t in turns),
|
|
84
|
+
"latency_stages": stages,
|
|
85
|
+
"slowest_stage": slowest,
|
|
86
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Scheduling. API model: first come, first served, one line per API key. Local model: preemptive priority (agent, then chat,
|
|
2
|
+
then other), first come first served inside each class. Code work: a small pool of slots beside both."""
|
|
3
|
+
from pal.llm_scheduler.api import ApiLine, ApiScheduler, is_rate_limit
|
|
4
|
+
from pal.llm_scheduler.code import CodeSlots
|
|
5
|
+
from pal.llm_scheduler.core import ScheduledWrapper, current_stats, task, track
|
|
6
|
+
from pal.llm_scheduler.local import LocalScheduler
|
|
7
|
+
from pal.llm_scheduler.pipeline import LLMScheduler
|
|
8
|
+
from pal.llm_scheduler.types import ExpiredError, QueueFullError, SchedulerError, TaskClass, Ticket, TurnStats
|
|
9
|
+
|
|
10
|
+
__all__ = ["ApiLine", "ApiScheduler", "CodeSlots", "ExpiredError", "LocalScheduler", "QueueFullError", "ScheduledWrapper",
|
|
11
|
+
"LLMScheduler", "SchedulerError", "TaskClass", "Ticket", "TurnStats", "current_stats", "is_rate_limit", "task", "track"]
|
pal/llm_scheduler/api.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""The API model's scheduler: first come, first served, one call at a time, per API key.
|
|
2
|
+
|
|
3
|
+
With one key there is one line, and chat, the coding agent and the documentation agent all wait in it, strictly in the order
|
|
4
|
+
they arrive. Two things protect a free tier: calls start at least `interval` seconds apart, and a rate-limit error (429,
|
|
5
|
+
quota) pauses the line and retries the same call after a growing delay instead of failing everyone behind it.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
from collections import deque
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from pal.llm_scheduler.types import QueueFullError, SchedulerError, Ticket
|
|
17
|
+
|
|
18
|
+
log = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def is_rate_limit(error: BaseException) -> bool:
|
|
22
|
+
for attr in ("status_code", "code"):
|
|
23
|
+
if getattr(error, attr, None) == 429:
|
|
24
|
+
return True
|
|
25
|
+
resp = getattr(error, "response", None)
|
|
26
|
+
if getattr(resp, "status_code", None) == 429:
|
|
27
|
+
return True
|
|
28
|
+
low = str(error).lower()
|
|
29
|
+
return any(k in low for k in ("429", "resource_exhausted", "rate limit", "quota"))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _ApiJob:
|
|
33
|
+
def __init__(self, ticket: Ticket, fn: Callable[[], Any]):
|
|
34
|
+
self.ticket, self.fn = ticket, fn
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ApiLine:
|
|
38
|
+
def __init__(self, interval: float, retries: int, backoff: float, max_queue: int):
|
|
39
|
+
self.interval, self.retries, self.backoff, self.max_queue = interval, retries, backoff, max_queue
|
|
40
|
+
self._queue: deque[_ApiJob] = deque()
|
|
41
|
+
self._cond = threading.Condition()
|
|
42
|
+
self._closed = False
|
|
43
|
+
self._thread = threading.Thread(target=self._worker, name="pal-api-line", daemon=True)
|
|
44
|
+
self._thread.start()
|
|
45
|
+
|
|
46
|
+
def submit(self, fn: Callable[[], Any], label: str = "api call") -> Ticket:
|
|
47
|
+
t = Ticket(label)
|
|
48
|
+
with self._cond:
|
|
49
|
+
if self._closed:
|
|
50
|
+
raise SchedulerError("the scheduler is closed")
|
|
51
|
+
if len(self._queue) >= self.max_queue:
|
|
52
|
+
raise QueueFullError(f"too many API calls are waiting ({self.max_queue})")
|
|
53
|
+
self._queue.append(_ApiJob(t, fn))
|
|
54
|
+
self._cond.notify()
|
|
55
|
+
return t
|
|
56
|
+
|
|
57
|
+
def pending(self) -> int:
|
|
58
|
+
with self._cond:
|
|
59
|
+
return len(self._queue)
|
|
60
|
+
|
|
61
|
+
def _worker(self) -> None:
|
|
62
|
+
last_start = 0.0
|
|
63
|
+
while True:
|
|
64
|
+
with self._cond:
|
|
65
|
+
while not self._queue:
|
|
66
|
+
if self._closed:
|
|
67
|
+
return
|
|
68
|
+
self._cond.wait()
|
|
69
|
+
job = self._queue.popleft()
|
|
70
|
+
gap = self.interval - (time.monotonic() - last_start)
|
|
71
|
+
if gap > 0:
|
|
72
|
+
time.sleep(gap)
|
|
73
|
+
t = job.ticket
|
|
74
|
+
t.started = last_start = time.monotonic()
|
|
75
|
+
attempt = 0
|
|
76
|
+
while True:
|
|
77
|
+
began = time.monotonic()
|
|
78
|
+
try:
|
|
79
|
+
value = job.fn()
|
|
80
|
+
except BaseException as e: # noqa: BLE001 (the caller gets the original error)
|
|
81
|
+
t.run_seconds += time.monotonic() - began
|
|
82
|
+
if is_rate_limit(e) and attempt < self.retries and not self._closed:
|
|
83
|
+
delay = self.backoff * (2 ** attempt)
|
|
84
|
+
attempt += 1
|
|
85
|
+
log.warning("rate limit on the API line: waiting %.1f s, then retry %d of %d", delay, attempt, self.retries)
|
|
86
|
+
time.sleep(delay)
|
|
87
|
+
last_start = time.monotonic()
|
|
88
|
+
continue
|
|
89
|
+
t.set_error(e)
|
|
90
|
+
break
|
|
91
|
+
t.run_seconds += time.monotonic() - began
|
|
92
|
+
t.set_result(value)
|
|
93
|
+
break
|
|
94
|
+
|
|
95
|
+
def close(self) -> None:
|
|
96
|
+
with self._cond:
|
|
97
|
+
self._closed = True
|
|
98
|
+
dropped = list(self._queue)
|
|
99
|
+
self._queue.clear()
|
|
100
|
+
self._cond.notify_all()
|
|
101
|
+
for j in dropped:
|
|
102
|
+
j.ticket.set_error(SchedulerError("the scheduler was closed"))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class ApiScheduler:
|
|
106
|
+
"""One ApiLine per API key (the key is part of the line name), created on first use."""
|
|
107
|
+
|
|
108
|
+
def __init__(self, retries: int = 2, backoff: float = 5.0, max_queue: int = 100, enabled: bool = True):
|
|
109
|
+
self.retries, self.backoff, self.max_queue, self.enabled = retries, backoff, max_queue, enabled
|
|
110
|
+
self._lines: dict[str, ApiLine] = {}
|
|
111
|
+
self._lock = threading.Lock()
|
|
112
|
+
|
|
113
|
+
def submit(self, line_key: str, fn: Callable[[], Any], interval: float = 0.0, label: str = "api call") -> Ticket:
|
|
114
|
+
if not self.enabled: # scheduling off: run right here, no line
|
|
115
|
+
t = Ticket(label)
|
|
116
|
+
t.started = time.monotonic()
|
|
117
|
+
try:
|
|
118
|
+
t.set_result(fn())
|
|
119
|
+
except BaseException as e: # noqa: BLE001
|
|
120
|
+
t.set_error(e)
|
|
121
|
+
return t
|
|
122
|
+
with self._lock:
|
|
123
|
+
line = self._lines.get(line_key)
|
|
124
|
+
if line is None:
|
|
125
|
+
line = self._lines[line_key] = ApiLine(interval, self.retries, self.backoff, self.max_queue)
|
|
126
|
+
line.interval = interval
|
|
127
|
+
return line.submit(fn, label)
|
|
128
|
+
|
|
129
|
+
def pending(self) -> dict[str, int]:
|
|
130
|
+
with self._lock:
|
|
131
|
+
return {k: ln.pending() for k, ln in self._lines.items()}
|
|
132
|
+
|
|
133
|
+
def close(self) -> None:
|
|
134
|
+
with self._lock:
|
|
135
|
+
lines, self._lines = list(self._lines.values()), {}
|
|
136
|
+
for ln in lines:
|
|
137
|
+
ln.close()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Code work (converting files, building the code graph) runs in code, beside the LLM lines. It does not wait for the model,
|
|
2
|
+
but it uses the CPU the local model also needs, so only a few run at once."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CodeSlots:
|
|
11
|
+
def __init__(self, workers: int = 2):
|
|
12
|
+
self._sem = threading.BoundedSemaphore(workers) if workers > 0 else None
|
|
13
|
+
self._lock = threading.Lock()
|
|
14
|
+
self.running = 0
|
|
15
|
+
self.peak = 0
|
|
16
|
+
|
|
17
|
+
def run(self, fn: Callable[[], Any]) -> Any:
|
|
18
|
+
if self._sem is None:
|
|
19
|
+
return fn()
|
|
20
|
+
with self._sem:
|
|
21
|
+
with self._lock:
|
|
22
|
+
self.running += 1
|
|
23
|
+
self.peak = max(self.peak, self.running)
|
|
24
|
+
try:
|
|
25
|
+
return fn()
|
|
26
|
+
finally:
|
|
27
|
+
with self._lock:
|
|
28
|
+
self.running -= 1
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""ScheduledWrapper: puts the local model behind the local scheduler without changing the code that uses it.
|
|
2
|
+
|
|
3
|
+
Anything that calls `core.chat(...)` (the router, the input savers, the verifier) keeps working; the call becomes a job in the
|
|
4
|
+
right class. The class comes from the turn that is running (`task(...)`), set once by the chat service.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import contextvars
|
|
9
|
+
from collections.abc import Iterator
|
|
10
|
+
from contextlib import contextmanager
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from pal.llm_scheduler.local import LocalScheduler
|
|
14
|
+
from pal.llm_scheduler.types import TaskClass, TurnStats
|
|
15
|
+
from pal.llm_wrapper.components.base import LLMResponse, LLMSyscall, LLMWrapper, WrapperCapabilities
|
|
16
|
+
from pal.tokens import estimate_tokens
|
|
17
|
+
|
|
18
|
+
_task: contextvars.ContextVar[tuple[TaskClass, bool]] = contextvars.ContextVar("pal_task", default=(TaskClass.CHAT, False))
|
|
19
|
+
_stats: contextvars.ContextVar[TurnStats | None] = contextvars.ContextVar("pal_turn_stats", default=None)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@contextmanager
|
|
23
|
+
def task(task_class: TaskClass, preemptible: bool = False) -> Iterator[None]:
|
|
24
|
+
"""Everything the local model is asked inside this block belongs to `task_class`. With `preemptible`, a plain-text
|
|
25
|
+
generation may be paused and resumed (only for cores that really stream)."""
|
|
26
|
+
token = _task.set((task_class, preemptible))
|
|
27
|
+
try:
|
|
28
|
+
yield
|
|
29
|
+
finally:
|
|
30
|
+
_task.reset(token)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@contextmanager
|
|
34
|
+
def track() -> Iterator[TurnStats]:
|
|
35
|
+
stats = TurnStats()
|
|
36
|
+
token = _stats.set(stats)
|
|
37
|
+
try:
|
|
38
|
+
yield stats
|
|
39
|
+
finally:
|
|
40
|
+
_stats.reset(token)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def current_stats() -> TurnStats | None:
|
|
44
|
+
return _stats.get()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _streams(core: LLMWrapper) -> bool:
|
|
48
|
+
return type(core).stream_chat is not LLMWrapper.stream_chat
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ScheduledWrapper(LLMWrapper):
|
|
52
|
+
name = "scheduled"
|
|
53
|
+
|
|
54
|
+
def __init__(self, core: LLMWrapper, scheduler: LocalScheduler | None):
|
|
55
|
+
self.core = core
|
|
56
|
+
self.scheduler = scheduler
|
|
57
|
+
self.config: Any = getattr(core, "config", None)
|
|
58
|
+
|
|
59
|
+
def chat(self, syscall: LLMSyscall) -> LLMResponse:
|
|
60
|
+
if self.scheduler is None:
|
|
61
|
+
return self.core.chat(syscall)
|
|
62
|
+
cls, preemptible = _task.get()
|
|
63
|
+
if preemptible and _streams(self.core):
|
|
64
|
+
ticket = self.scheduler.submit_stream(self.core, syscall, cls)
|
|
65
|
+
text = ticket.result()
|
|
66
|
+
prompt = estimate_tokens(" ".join(str(m.get("content", "")) for m in syscall.messages))
|
|
67
|
+
resp = LLMResponse(text=text, prompt_tokens=prompt, completion_tokens=estimate_tokens(text),
|
|
68
|
+
meta={"streamed": True, "pauses": ticket.pauses})
|
|
69
|
+
else:
|
|
70
|
+
ticket = self.scheduler.submit(lambda: self.core.chat(syscall), cls)
|
|
71
|
+
resp = ticket.result()
|
|
72
|
+
stats = _stats.get()
|
|
73
|
+
if stats is not None:
|
|
74
|
+
stats.add_local(ticket)
|
|
75
|
+
return resp
|
|
76
|
+
|
|
77
|
+
def stream_chat(self, syscall: LLMSyscall):
|
|
78
|
+
return self.core.stream_chat(syscall)
|
|
79
|
+
|
|
80
|
+
def token_count(self, text: str) -> int:
|
|
81
|
+
return self.core.token_count(text)
|
|
82
|
+
|
|
83
|
+
def embed(self, texts: list[str]) -> list[list[float]]:
|
|
84
|
+
return self.core.embed(texts)
|
|
85
|
+
|
|
86
|
+
def capabilities(self) -> WrapperCapabilities:
|
|
87
|
+
return self.core.capabilities()
|
|
88
|
+
|
|
89
|
+
def __getattr__(self, name: str) -> Any: # e.g. init_beam_search on in-process cores
|
|
90
|
+
if name in ("core", "scheduler", "config"):
|
|
91
|
+
raise AttributeError(name)
|
|
92
|
+
return getattr(self.core, name)
|