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
pal/cli.py
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
"""Command line: `pal serve`, `pal doctor`, `pal config` (also `python -m pal ...`).
|
|
2
|
+
|
|
3
|
+
Works the same on Linux, macOS and Windows: pure Python, no shell scripts.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import importlib.util
|
|
9
|
+
import os
|
|
10
|
+
import shutil
|
|
11
|
+
import sys
|
|
12
|
+
from collections.abc import Callable, Sequence
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
import yaml
|
|
16
|
+
|
|
17
|
+
from pal import __version__
|
|
18
|
+
from pal.agents import AGENTS
|
|
19
|
+
from pal.config import Settings, load_settings
|
|
20
|
+
from pal.errors import ConfigError, PalError
|
|
21
|
+
from pal.logging_setup import configure_logging
|
|
22
|
+
from pal.paths import pal_home
|
|
23
|
+
from pal.sdk.agent import agent_key
|
|
24
|
+
|
|
25
|
+
_LOOPBACK = ("127.0.0.1", "localhost", "::1")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class Check:
|
|
30
|
+
name: str
|
|
31
|
+
status: str # "ok" | "warn" | "fail" | "info"
|
|
32
|
+
detail: str = ""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _services_check(settings: Settings, env) -> Check:
|
|
36
|
+
"""Which third-party tools are switched on, and which have their key."""
|
|
37
|
+
from pal.chat.services import selected
|
|
38
|
+
try:
|
|
39
|
+
specs = selected(settings.tools.services)
|
|
40
|
+
except ConfigError as e:
|
|
41
|
+
return Check("service tools", "fail", str(e))
|
|
42
|
+
if not specs:
|
|
43
|
+
return Check("service tools", "info", "off (name them in tools.services, or all: google_search, wolfram_alpha, currency_convert, "
|
|
44
|
+
"words_api, moon_phase, and the image and audio tools)")
|
|
45
|
+
missing: dict[str, list[str]] = {}
|
|
46
|
+
for s in specs:
|
|
47
|
+
if s.key_env and not env.get(s.key_env):
|
|
48
|
+
missing.setdefault(s.key_env, []).append(s.name)
|
|
49
|
+
ready = [s.name for s in specs if not s.key_env or env.get(s.key_env)]
|
|
50
|
+
detail = f"{len(ready)} of {len(specs)} ready" + (f" ({', '.join(ready)})" if ready else "")
|
|
51
|
+
if missing:
|
|
52
|
+
detail += "; missing: " + "; ".join(f"{k} for {', '.join(v)}" for k, v in missing.items())
|
|
53
|
+
return Check("service tools", "warn" if missing else "ok", detail)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _installed_version(name: str) -> str | None:
|
|
57
|
+
try:
|
|
58
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
59
|
+
return version(name)
|
|
60
|
+
except PackageNotFoundError:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _has_module(name: str) -> bool:
|
|
65
|
+
try:
|
|
66
|
+
return importlib.util.find_spec(name) is not None
|
|
67
|
+
except (ImportError, ValueError):
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def run_doctor(settings: Settings | None, env=None, which: Callable[[str], str | None] = shutil.which,
|
|
72
|
+
has_module: Callable[[str], bool] = _has_module, http_get=None,
|
|
73
|
+
version_of: Callable[[str], str | None] = _installed_version) -> list[Check]:
|
|
74
|
+
"""Environment checks. Everything the OS or network is asked is injectable, so this is testable."""
|
|
75
|
+
env = os.environ if env is None else env
|
|
76
|
+
out: list[Check] = []
|
|
77
|
+
v = sys.version_info
|
|
78
|
+
out.append(Check("python", "ok" if v >= (3, 10) else "fail", f"{v.major}.{v.minor}.{v.micro} (needs 3.10+)"))
|
|
79
|
+
out.append(Check("platform", "info", f"{sys.platform}, pal-code {__version__}"))
|
|
80
|
+
if settings is None:
|
|
81
|
+
return out
|
|
82
|
+
|
|
83
|
+
home = pal_home()
|
|
84
|
+
try:
|
|
85
|
+
home.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
probe = home / ".write-test"
|
|
87
|
+
probe.write_text("x", encoding="utf-8")
|
|
88
|
+
probe.unlink()
|
|
89
|
+
out.append(Check("data folder", "ok", f"{home} is writable"))
|
|
90
|
+
except OSError as e:
|
|
91
|
+
out.append(Check("data folder", "fail", f"{home}: {e}"))
|
|
92
|
+
|
|
93
|
+
out.append(Check("pandoc", "ok" if which("pandoc") else "warn",
|
|
94
|
+
"found" if which("pandoc") else "not found: HTML/DOCX/ODT/RTF/EPUB/LaTeX attachments will be "
|
|
95
|
+
"rejected (PDF, PPTX, XLSX and text still work). Install: https://pandoc.org/installing.html"))
|
|
96
|
+
for label, module, why in (("pypdf", "pypdf", "PDF attachments"), ("python-pptx", "pptx", "PowerPoint attachments"),
|
|
97
|
+
("openpyxl", "openpyxl", "Excel attachments"), ("graphify", "graphify", "code-project graphs"),
|
|
98
|
+
("waitress", "waitress", "production server (Flask dev server is the fallback)")):
|
|
99
|
+
ok = has_module(module)
|
|
100
|
+
out.append(Check(label, "ok" if ok else "warn", "installed" if ok else f"missing: no {why}. pip install pal-code[full]"))
|
|
101
|
+
for label, module, why in (("chromadb", "chromadb", "documents are kept in memory instead of a disk vector store"),
|
|
102
|
+
("sentence-transformers", "sentence_transformers", "keyword matching is used instead of embeddings")):
|
|
103
|
+
out.append(Check(label, "ok" if has_module(module) else "info", "installed" if has_module(module) else f"optional: {why}"))
|
|
104
|
+
|
|
105
|
+
llm = settings.local_llm
|
|
106
|
+
if llm.get("backend") == "ollama":
|
|
107
|
+
host = (llm.get("host") or "http://localhost:11434").rstrip("/")
|
|
108
|
+
try:
|
|
109
|
+
get = http_get or _http_get_json
|
|
110
|
+
names = [m["name"] for m in get(host + "/api/tags").get("models", [])]
|
|
111
|
+
have = any(n == llm["model"] or n.startswith(llm["model"] + ":") for n in names)
|
|
112
|
+
out.append(Check("local LLM", "ok" if have else "warn",
|
|
113
|
+
f"{llm['model']} ready" if have else f"Ollama is up but {llm['model']} is not pulled: ollama pull {llm['model']}"))
|
|
114
|
+
except Exception as e: # noqa: BLE001 (any failure = unreachable)
|
|
115
|
+
out.append(Check("local LLM", "warn", f"Ollama not reachable at {host} ({e.__class__.__name__}): "
|
|
116
|
+
"routing and input savers need it; every request will fail until it runs"))
|
|
117
|
+
else:
|
|
118
|
+
out.append(Check("local LLM", "info", f"{llm.get('backend')} (not checked)"))
|
|
119
|
+
|
|
120
|
+
from pal.chat.providers import API_PROVIDERS
|
|
121
|
+
have_keys = [p for p, var in API_PROVIDERS.items() if env.get(var)]
|
|
122
|
+
out.append(Check("API keys", "ok" if have_keys else "info",
|
|
123
|
+
", ".join(have_keys) if have_keys else "none in the environment (you can paste one in the UI)"))
|
|
124
|
+
|
|
125
|
+
from pal.llm_wrapper.modes import describe_modes
|
|
126
|
+
for mode in describe_modes(env):
|
|
127
|
+
with_keys = ", ".join(p["name"] for p in mode["providers"] if p["key_set"])
|
|
128
|
+
want = " or ".join(p["key_env"] for p in mode["providers"][:2]) + (" ..." if len(mode["providers"]) > 2 else "")
|
|
129
|
+
out.append(Check(f"mode: {mode['title'].lower()}", "ok" if mode["ready"] else "info",
|
|
130
|
+
f"ready ({with_keys})" if mode["ready"] else f"not set up: set {want}"))
|
|
131
|
+
|
|
132
|
+
from pal.llm_wrapper.components.litellm_wrapper import PINNED
|
|
133
|
+
ll_version = version_of("litellm")
|
|
134
|
+
if ll_version is None:
|
|
135
|
+
out.append(Check("litellm", "warn", f"not installed: cloud APIs need it. Run python run.py, or pip install --require-hashes "
|
|
136
|
+
f"-r requirements/litellm.lock (pinned to {PINNED})"))
|
|
137
|
+
elif ll_version != PINNED:
|
|
138
|
+
out.append(Check("litellm", "warn", f"version {ll_version} is installed but this release is pinned to {PINNED}. Reinstall "
|
|
139
|
+
"from requirements/litellm.lock (the pin was reviewed; other versions were not)"))
|
|
140
|
+
else:
|
|
141
|
+
out.append(Check("litellm", "ok", f"{ll_version} (pinned; the SDK only, never the proxy server)"))
|
|
142
|
+
fb = settings.fallback
|
|
143
|
+
if not fb.enabled:
|
|
144
|
+
out.append(Check("fallback", "info", "off (set fallback.enabled and fallback.model to answer with a second model when "
|
|
145
|
+
"your own provider is rate-limited or down)"))
|
|
146
|
+
elif not fb.model:
|
|
147
|
+
out.append(Check("fallback", "warn", "enabled but fallback.model is empty, so it does nothing"))
|
|
148
|
+
else:
|
|
149
|
+
var = API_PROVIDERS.get(fb.provider)
|
|
150
|
+
if var and not env.get(var):
|
|
151
|
+
out.append(Check("fallback", "warn", f"{fb.provider}/{fb.model} is set but {var} is not in the environment"))
|
|
152
|
+
else:
|
|
153
|
+
out.append(Check("fallback", "ok", f"{fb.provider}/{fb.model} answers when your own provider is rate-limited or down"))
|
|
154
|
+
|
|
155
|
+
out.append(_services_check(settings, env))
|
|
156
|
+
|
|
157
|
+
for cls in AGENTS.values():
|
|
158
|
+
spec = cls.spec
|
|
159
|
+
if spec is None:
|
|
160
|
+
continue
|
|
161
|
+
ok = bool(agent_key(spec, env))
|
|
162
|
+
out.append(Check(f"agent: {spec.id}", "ok" if ok else "info",
|
|
163
|
+
"key found" if ok else f"set {spec.key_env} in .env to use the {spec.name}"))
|
|
164
|
+
|
|
165
|
+
from pal.os_kernel import system_info
|
|
166
|
+
info = system_info()
|
|
167
|
+
mem = (info.get("memory") or {}).get("total_mb")
|
|
168
|
+
out.append(Check("OS kernel", "info", f"{info['distro']}, kernel {info['kernel']}, {info['cpu_count']} CPUs"
|
|
169
|
+
+ (f", {mem} MB RAM" if mem else "") + " (non-LLM queries go here)"))
|
|
170
|
+
|
|
171
|
+
from pal.orchestrator.components.cgroup_manager import CgroupManager
|
|
172
|
+
enforced = CgroupManager.supported()
|
|
173
|
+
out.append(Check("agent resource limits", "info", "cgroup v2 available" if enforced else
|
|
174
|
+
"not enforced on this OS (only matters for the multi-agent orchestrator, not the chat)"))
|
|
175
|
+
return out
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _http_get_json(url: str) -> dict:
|
|
179
|
+
import requests
|
|
180
|
+
return requests.get(url, timeout=3).json()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _print_checks(checks: list[Check], stream=None) -> None:
|
|
184
|
+
stream = stream or sys.stdout
|
|
185
|
+
mark = {"ok": "ok ", "warn": "warn", "fail": "FAIL", "info": "info"}
|
|
186
|
+
for c in checks:
|
|
187
|
+
print(f"[{mark[c.status]}] {c.name}: {c.detail}", file=stream)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def load_dotenv(path: str | os.PathLike | None = None, env=None) -> list[str]:
|
|
191
|
+
"""Read KEY=VALUE lines from a .env file into the environment. Never overrides a variable that is already
|
|
192
|
+
set. Returns the names it set (values are never printed or logged)."""
|
|
193
|
+
from pathlib import Path
|
|
194
|
+
env = os.environ if env is None else env
|
|
195
|
+
p = Path(path) if path else Path.cwd() / ".env"
|
|
196
|
+
if not p.is_file():
|
|
197
|
+
return []
|
|
198
|
+
loaded = []
|
|
199
|
+
for raw in p.read_text(encoding="utf-8-sig").splitlines():
|
|
200
|
+
line = raw.strip()
|
|
201
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
202
|
+
continue
|
|
203
|
+
key, _, value = line.removeprefix("export ").partition("=")
|
|
204
|
+
key, value = key.strip(), value.strip()
|
|
205
|
+
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
|
|
206
|
+
value = value[1:-1]
|
|
207
|
+
elif " #" in value:
|
|
208
|
+
value = value.split(" #", 1)[0].rstrip()
|
|
209
|
+
if key and value and key not in env:
|
|
210
|
+
env[key] = value
|
|
211
|
+
loaded.append(key)
|
|
212
|
+
return loaded
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _serve(settings: Settings, open_browser: bool = False, page: str = "", graph=None, documents: list | None = None) -> int:
|
|
216
|
+
from pal.chat import ChatService
|
|
217
|
+
from pal.web import create_app
|
|
218
|
+
|
|
219
|
+
s = settings.server
|
|
220
|
+
configure_logging(s.log_level)
|
|
221
|
+
service = ChatService(settings, graph=graph) if graph is not None else ChatService(settings)
|
|
222
|
+
if documents:
|
|
223
|
+
service.preload_docs = list(documents)
|
|
224
|
+
app = create_app(service)
|
|
225
|
+
if s.host not in _LOOPBACK:
|
|
226
|
+
print(f"WARNING: listening on {s.host}. Pal has no login: anyone who can reach this port can use "
|
|
227
|
+
"your API keys' quota and read stored memory. Put it behind an authenticating reverse proxy.",
|
|
228
|
+
file=sys.stderr)
|
|
229
|
+
shown = "localhost" if s.host in ("0.0.0.0", "::") else s.host # noqa: S104
|
|
230
|
+
url = f"http://{shown}:{s.port}" + ("/" + page.strip("/") if page else "")
|
|
231
|
+
print(f"Pal {__version__} on {url} (Ctrl+C to stop)")
|
|
232
|
+
from pal.chat.providers import API_PROVIDERS
|
|
233
|
+
keys = [p for p, var in API_PROVIDERS.items() if os.environ.get(var)]
|
|
234
|
+
print("API key found for: " + ", ".join(keys) if keys else
|
|
235
|
+
"No API key in the environment: paste one in the page (API key menu), or set e.g. GEMINI_API_KEY.")
|
|
236
|
+
status = service.local_status()
|
|
237
|
+
if status["ok"] is False:
|
|
238
|
+
print(f"WARNING: local model: {status['detail']}. Input reduction and local answers are off until it runs; "
|
|
239
|
+
"the app still works and sends everything to the API model.", file=sys.stderr)
|
|
240
|
+
else:
|
|
241
|
+
print(f"Local model: {status['detail']}")
|
|
242
|
+
if open_browser:
|
|
243
|
+
import threading
|
|
244
|
+
import webbrowser
|
|
245
|
+
threading.Timer(1.2, lambda: webbrowser.open(url)).start()
|
|
246
|
+
try:
|
|
247
|
+
try:
|
|
248
|
+
from waitress import serve
|
|
249
|
+
except ImportError:
|
|
250
|
+
print("waitress is not installed: using Flask's development server (pip install waitress)",
|
|
251
|
+
file=sys.stderr)
|
|
252
|
+
app.run(host=s.host, port=s.port, threaded=True)
|
|
253
|
+
else:
|
|
254
|
+
serve(app, host=s.host, port=s.port, threads=s.threads, ident="pal")
|
|
255
|
+
finally:
|
|
256
|
+
service.close()
|
|
257
|
+
return 0
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class _UploadFile:
|
|
261
|
+
"""Just enough of an uploaded file for the attachment reader."""
|
|
262
|
+
|
|
263
|
+
def __init__(self, path):
|
|
264
|
+
from pathlib import Path
|
|
265
|
+
self._path = Path(path)
|
|
266
|
+
self.filename = self._path.name
|
|
267
|
+
|
|
268
|
+
def read(self) -> bytes:
|
|
269
|
+
try:
|
|
270
|
+
return self._path.read_bytes()
|
|
271
|
+
except OSError as e:
|
|
272
|
+
raise ConfigError(f"cannot read {self._path}: {e.strerror or e}") from e
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _agent(settings: Settings, name: str, message: str, files: list[str]) -> int:
|
|
276
|
+
"""One agent turn from the terminal. The same pipeline, token savers included."""
|
|
277
|
+
from pal.chat import ChatService
|
|
278
|
+
from pal.chat.uploads import read_upload
|
|
279
|
+
|
|
280
|
+
service = ChatService(settings)
|
|
281
|
+
try:
|
|
282
|
+
attachments = [read_upload(service, _UploadFile(p), "cli") for p in files]
|
|
283
|
+
res = service.send("cli", "", "", None, message, attachments, agent=name)
|
|
284
|
+
except PalError as e:
|
|
285
|
+
print(f"error: {e}", file=sys.stderr)
|
|
286
|
+
return 1
|
|
287
|
+
finally:
|
|
288
|
+
service.close()
|
|
289
|
+
print(res.reply)
|
|
290
|
+
print(f"\n[{res.input_tokens} in / {res.output_tokens} out tokens, {res.latency.get('total', 0)} s]", file=sys.stderr)
|
|
291
|
+
return 0
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
_CHAT_HELP = ("/file PATH attach a file or source zip to your next message\n/agent NAME talk to an agent (coding, docs); "
|
|
295
|
+
"/agent none for plain chat\n/reset start a new conversation\n/stats what has been saved so far\n"
|
|
296
|
+
"/help this list\n/quit leave")
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def render_models(models: list, free_only: bool = False) -> str:
|
|
300
|
+
"""OpenRouter's models as a plain table: the name to pass as --model, the context size, the price, and whether it is free."""
|
|
301
|
+
if not models:
|
|
302
|
+
return "No models match.\n"
|
|
303
|
+
width = max(len(m.id) for m in models)
|
|
304
|
+
rows = [f"{'MODEL (pass as --model)':<{width}} {'CONTEXT':>8} PRICE (USD per million tokens in / out)"]
|
|
305
|
+
for m in models:
|
|
306
|
+
ctx = f"{m.context_length:,}" if m.context_length else "?"
|
|
307
|
+
price = "free" if m.free else (f"{m.prompt_per_million:g} / {m.completion_per_million:g}" if m.prompt_per_million is not None and m.completion_per_million is not None else "?")
|
|
308
|
+
rows.append(f"{m.id:<{width}} {ctx:>8} {price}")
|
|
309
|
+
rows.append(f"\n{len(models)} model(s). Use one with: pal chat --mode explore --model <MODEL>")
|
|
310
|
+
return "\n".join(rows) + "\n"
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _models(args, fetch=None) -> int:
|
|
314
|
+
"""pal models [--free] [--search WORDS] [--limit N] [--json]: browse OpenRouter's models (needs no key, needs the network)."""
|
|
315
|
+
from pal.llm_wrapper.components.openrouter_wrapper import fetch_models, search_models
|
|
316
|
+
try:
|
|
317
|
+
found = search_models(fetch_models(fetch), args.search or "", args.free, args.limit)
|
|
318
|
+
except Exception as e: # noqa: BLE001
|
|
319
|
+
print(f"could not read OpenRouter's model list ({type(e).__name__}: {e}). Check your network, or type a model name yourself.", file=sys.stderr)
|
|
320
|
+
return 1
|
|
321
|
+
if args.json:
|
|
322
|
+
import json
|
|
323
|
+
from dataclasses import asdict
|
|
324
|
+
print(json.dumps([asdict(m) for m in found], indent=2))
|
|
325
|
+
else:
|
|
326
|
+
print(render_models(found, args.free), end="")
|
|
327
|
+
return 0
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _pick_provider(settings: Settings, provider: str | None, model: str | None, env, mode: str | None = None) -> tuple[str, str]:
|
|
331
|
+
"""Which API provider and model plain chat uses: what you asked for, else the first provider that has a key. `mode` (explore or own_key)
|
|
332
|
+
limits the choice to that mode's providers."""
|
|
333
|
+
from pal.chat.providers import API_PROVIDERS
|
|
334
|
+
from pal.llm_wrapper.modes import MODES, mode_for
|
|
335
|
+
if mode and provider and mode_for(provider) != mode:
|
|
336
|
+
raise ConfigError(f"{provider} is not an {MODES[mode].title.lower()} provider (mode {mode}: {', '.join(MODES[mode].providers)})")
|
|
337
|
+
if not provider:
|
|
338
|
+
allowed = MODES[mode].providers if mode else tuple(API_PROVIDERS)
|
|
339
|
+
provider = next((p for p in allowed if env.get(API_PROVIDERS[p])), "")
|
|
340
|
+
if not provider and mode == "explore":
|
|
341
|
+
raise ConfigError("Explore mode needs an OpenRouter key: set OPENROUTER_API_KEY (pal models lists what it can reach)")
|
|
342
|
+
if not provider and mode:
|
|
343
|
+
raise ConfigError("own-key mode needs a key for one provider: set for example GEMINI_API_KEY, or pass --provider")
|
|
344
|
+
if not provider:
|
|
345
|
+
raise ConfigError("no API key found: set one (for example GEMINI_API_KEY) or pass --provider")
|
|
346
|
+
if not model:
|
|
347
|
+
if provider != "gemini":
|
|
348
|
+
raise ConfigError(f"choose a model: --model <name> (for {provider})")
|
|
349
|
+
model = settings.agents.model
|
|
350
|
+
return provider, model
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _chat(settings: Settings, agent: str | None, provider: str | None, model: str | None, files: list[str],
|
|
354
|
+
input_fn: Callable[[str], str] | None = None, env=None, mode: str | None = None) -> int:
|
|
355
|
+
"""An interactive terminal chat: the same pipeline as the web page (token savers, scheduler, agents)."""
|
|
356
|
+
from pal.chat import ChatService
|
|
357
|
+
from pal.chat.uploads import read_upload
|
|
358
|
+
|
|
359
|
+
env = os.environ if env is None else env
|
|
360
|
+
read = input_fn if input_fn is not None else input # looked up now, so it can be replaced (and tested)
|
|
361
|
+
service = ChatService(settings)
|
|
362
|
+
session, pending = "terminal", []
|
|
363
|
+
try:
|
|
364
|
+
provider, model = _pick_provider(settings, provider, model, env, mode) if not agent else (provider or "", model or "")
|
|
365
|
+
print(f"Pal chat ({'agent: ' + agent if agent else provider + '/' + model}). /help lists commands.", file=sys.stderr)
|
|
366
|
+
for p in files:
|
|
367
|
+
pending.append(read_upload(service, _UploadFile(p), session))
|
|
368
|
+
while True:
|
|
369
|
+
try:
|
|
370
|
+
line = read("you> ").strip()
|
|
371
|
+
except (EOFError, KeyboardInterrupt):
|
|
372
|
+
print()
|
|
373
|
+
return 0
|
|
374
|
+
if not line:
|
|
375
|
+
continue
|
|
376
|
+
if line.startswith("/"):
|
|
377
|
+
cmd, _, arg = line.partition(" ")
|
|
378
|
+
if cmd in ("/quit", "/exit"):
|
|
379
|
+
return 0
|
|
380
|
+
if cmd == "/help":
|
|
381
|
+
print(_CHAT_HELP)
|
|
382
|
+
elif cmd == "/reset":
|
|
383
|
+
service.reset(session)
|
|
384
|
+
pending.clear()
|
|
385
|
+
print("new conversation")
|
|
386
|
+
elif cmd == "/agent":
|
|
387
|
+
name = arg.strip()
|
|
388
|
+
if name in ("", "none"):
|
|
389
|
+
agent = None
|
|
390
|
+
provider, model = _pick_provider(settings, provider, model, env, mode)
|
|
391
|
+
elif name in AGENTS:
|
|
392
|
+
agent = name
|
|
393
|
+
else:
|
|
394
|
+
print(f"unknown agent {name!r}; choose one of: {', '.join(AGENTS)}")
|
|
395
|
+
continue
|
|
396
|
+
print(f"now talking to: {agent or 'plain chat'}")
|
|
397
|
+
elif cmd == "/file":
|
|
398
|
+
try:
|
|
399
|
+
pending.append(read_upload(service, _UploadFile(arg.strip()), session))
|
|
400
|
+
print(f"attached {pending[-1].get('name', arg.strip())}; it goes with your next message")
|
|
401
|
+
except PalError as e:
|
|
402
|
+
print(f"error: {e}")
|
|
403
|
+
elif cmd == "/stats":
|
|
404
|
+
s = service.metrics.chat_summary(session)
|
|
405
|
+
print(f"turns {s.get('turns', 0)} | tokens saved (estimate) {s.get('tokens_saved_estimate', {})} | "
|
|
406
|
+
f"cost ${s.get('cost_usd', 0)} | saved about ${s.get('saved_cost_usd', 0)}")
|
|
407
|
+
else:
|
|
408
|
+
print("unknown command; /help lists them")
|
|
409
|
+
continue
|
|
410
|
+
try:
|
|
411
|
+
res = service.send(session, provider or "", model or "", None, line, pending, agent=agent)
|
|
412
|
+
except PalError as e:
|
|
413
|
+
print(f"error: {e}")
|
|
414
|
+
continue
|
|
415
|
+
pending = []
|
|
416
|
+
print(res.reply)
|
|
417
|
+
note = [res.handled_by, f"{res.input_tokens} in / {res.output_tokens} out"]
|
|
418
|
+
if res.fallback:
|
|
419
|
+
note.append(f"answered by fallback {res.fallback['provider']}/{res.fallback['model']}")
|
|
420
|
+
if res.cost_usd:
|
|
421
|
+
note.append(f"${res.cost_usd:.4f}")
|
|
422
|
+
print("[" + " | ".join(note) + "]", file=sys.stderr)
|
|
423
|
+
except PalError as e:
|
|
424
|
+
print(f"error: {e}", file=sys.stderr)
|
|
425
|
+
return 1
|
|
426
|
+
finally:
|
|
427
|
+
service.close()
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
431
|
+
p = argparse.ArgumentParser(prog="pal", description="Pal chat layer")
|
|
432
|
+
p.add_argument("--version", action="version", version=f"pal-code {__version__}")
|
|
433
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
434
|
+
for name, help_ in (("serve", "run the web chat"), ("doctor", "check this machine"),
|
|
435
|
+
("config", "print the effective settings"), ("agent", "run the coding or documentation agent"),
|
|
436
|
+
("chat", "chat in the terminal"), ("tools", "list every tool by name"),
|
|
437
|
+
("models", "browse the models OpenRouter can reach (Explore mode)")):
|
|
438
|
+
sp = sub.add_parser(name, help=help_)
|
|
439
|
+
sp.add_argument("--config", help="path to a settings YAML file (or set PAL_CONFIG)")
|
|
440
|
+
if name == "agent":
|
|
441
|
+
sp.add_argument("name", choices=list(AGENTS))
|
|
442
|
+
sp.add_argument("message")
|
|
443
|
+
sp.add_argument("--file", action="append", default=[], help="attach a file or source zip (repeatable)")
|
|
444
|
+
if name == "models":
|
|
445
|
+
sp.add_argument("--search", help="only models whose name contains these words")
|
|
446
|
+
sp.add_argument("--free", action="store_true", help="only free models")
|
|
447
|
+
sp.add_argument("--limit", type=int, default=30, help="at most this many (default 30; 0 for all)")
|
|
448
|
+
sp.add_argument("--json", action="store_true")
|
|
449
|
+
if name == "chat":
|
|
450
|
+
sp.add_argument("--agent", choices=list(AGENTS))
|
|
451
|
+
sp.add_argument("--mode", choices=["explore", "own_key"], help="explore: one OpenRouter key, many models. own_key: your key for one provider")
|
|
452
|
+
sp.add_argument("--provider")
|
|
453
|
+
sp.add_argument("--model")
|
|
454
|
+
sp.add_argument("--file", action="append", default=[], help="attach a file or source zip (repeatable)")
|
|
455
|
+
if name == "serve":
|
|
456
|
+
sp.add_argument("--host")
|
|
457
|
+
sp.add_argument("--port", type=int)
|
|
458
|
+
sp.add_argument("--open", action="store_true", help="open the page in your browser")
|
|
459
|
+
sp.add_argument("--page", choices=["", "studio"], default="", help="which page to open: the chat (default) or the Studio")
|
|
460
|
+
return p
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
464
|
+
args = build_parser().parse_args(argv)
|
|
465
|
+
load_dotenv()
|
|
466
|
+
try:
|
|
467
|
+
overrides: dict = {}
|
|
468
|
+
if args.cmd == "serve":
|
|
469
|
+
server = {k: v for k, v in (("host", args.host), ("port", args.port)) if v is not None}
|
|
470
|
+
overrides = {"server": server} if server else {}
|
|
471
|
+
settings = load_settings(args.config, overrides=overrides)
|
|
472
|
+
except ConfigError as e:
|
|
473
|
+
if args.cmd == "doctor":
|
|
474
|
+
_print_checks(run_doctor(None) + [Check("config", "fail", str(e))])
|
|
475
|
+
return 1
|
|
476
|
+
print(f"config error: {e}", file=sys.stderr)
|
|
477
|
+
return 2
|
|
478
|
+
|
|
479
|
+
if args.cmd == "config":
|
|
480
|
+
print(yaml.safe_dump(settings.to_dict(), sort_keys=False, allow_unicode=True), end="")
|
|
481
|
+
return 0
|
|
482
|
+
if args.cmd == "doctor":
|
|
483
|
+
checks = run_doctor(settings)
|
|
484
|
+
_print_checks(checks)
|
|
485
|
+
failed = [c for c in checks if c.status == "fail"]
|
|
486
|
+
print(f"\n{len(failed)} problem(s)." if failed else "\nReady. Run: pal cli (terminal) or pal web (browser)")
|
|
487
|
+
return 1 if failed else 0
|
|
488
|
+
if args.cmd == "agent":
|
|
489
|
+
return _agent(settings, args.name, args.message, args.file)
|
|
490
|
+
if args.cmd == "tools":
|
|
491
|
+
from pal.tool_catalog import render
|
|
492
|
+
print(render(settings.tools, os.environ), end="")
|
|
493
|
+
return 0
|
|
494
|
+
if args.cmd == "models":
|
|
495
|
+
return _models(args)
|
|
496
|
+
if args.cmd == "chat":
|
|
497
|
+
return _chat(settings, args.agent, args.provider, args.model, args.file, mode=args.mode)
|
|
498
|
+
return _serve(settings, open_browser=args.open, page=args.page)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
if __name__ == "__main__":
|
|
502
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""The Compression component: the NLP prompt compressor and the history compressor (old assistant replies, facts of old turns)."""
|
|
2
|
+
from pal.compression.history import (
|
|
3
|
+
HistoryCompressor,
|
|
4
|
+
LayeredHistory,
|
|
5
|
+
compact_reply,
|
|
6
|
+
composition,
|
|
7
|
+
extract_facts,
|
|
8
|
+
facts_block,
|
|
9
|
+
)
|
|
10
|
+
from pal.compression.pipeline import CompressionPipeline
|
|
11
|
+
|
|
12
|
+
__all__ = ["CompressionPipeline", "HistoryCompressor", "LayeredHistory", "compact_reply", "composition", "extract_facts", "facts_block"]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Optional NLP-backed components (nltk/spacy/proselint). Each import is
|
|
3
|
+
independently guarded: if a dependency isn't installed yet (before
|
|
4
|
+
install.sh's model-download step has run, or in a minimal/CI
|
|
5
|
+
environment), the corresponding name is simply left unavailable here
|
|
6
|
+
rather than crashing the entire pal import chain -- Python
|
|
7
|
+
imports a package's __init__.py as a side effect of importing ANY of
|
|
8
|
+
its submodules, so this file being fragile would break unrelated
|
|
9
|
+
components too (llm_wrapper, orchestrator, ...) that have nothing to do
|
|
10
|
+
with text compression.
|
|
11
|
+
|
|
12
|
+
pal.compression.pipeline.CompressionPipeline already degrades
|
|
13
|
+
gracefully (falls back to whitespace/dedupe-only compression) when
|
|
14
|
+
TextMinifier itself can't be built for the same reason.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from pal.compression.components.adverbs import Adverbs
|
|
19
|
+
except ImportError:
|
|
20
|
+
Adverbs = None
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from pal.compression.components.hedges import Hedges
|
|
24
|
+
except ImportError:
|
|
25
|
+
Hedges = None
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
from pal.compression.components.interjections import Interjections
|
|
29
|
+
except ImportError:
|
|
30
|
+
Interjections = None
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
from pal.compression.components.pleonasms import Pleonasms
|
|
34
|
+
except ImportError:
|
|
35
|
+
Pleonasms = None
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
from pal.compression.components.punctuations import Punctuations
|
|
39
|
+
except ImportError:
|
|
40
|
+
Punctuations = None
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
from pal.compression.components.stopwords import Stopwords
|
|
44
|
+
except ImportError:
|
|
45
|
+
Stopwords = None
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
from pal.compression.components.minifier import TextMinifier
|
|
49
|
+
except ImportError:
|
|
50
|
+
TextMinifier = None
|
|
51
|
+
|
|
52
|
+
# No optional deps -- always available.
|
|
53
|
+
from pal.compression.components.output_compressor import OutputCompressor
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"Adverbs",
|
|
57
|
+
"Hedges",
|
|
58
|
+
"Interjections",
|
|
59
|
+
"TextMinifier",
|
|
60
|
+
"OutputCompressor",
|
|
61
|
+
"Pleonasms",
|
|
62
|
+
"Punctuations",
|
|
63
|
+
"Stopwords",
|
|
64
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pal.compression.utils.spacy_setup import SpacySetup
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Adverbs:
|
|
7
|
+
def __init__(self, spacy_setup: SpacySetup | None = None) -> None:
|
|
8
|
+
self.spacy_setup = spacy_setup or SpacySetup()
|
|
9
|
+
self.nlp = self.spacy_setup.load_model()
|
|
10
|
+
|
|
11
|
+
def _tokenize(self, text: str):
|
|
12
|
+
return self.nlp(text)
|
|
13
|
+
|
|
14
|
+
def _is_adverb(self, token) -> bool:
|
|
15
|
+
return token.pos_ == "ADV"
|
|
16
|
+
|
|
17
|
+
def remove(self, text: str) -> str:
|
|
18
|
+
doc = self._tokenize(text)
|
|
19
|
+
kept_tokens = [token.text for token in doc if not self._is_adverb(token)]
|
|
20
|
+
return " ".join(kept_tokens)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from proselint.tools import DEFAULT, LintFile
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Hedges:
|
|
7
|
+
RELEVANT_PREFIX: str = "hedging"
|
|
8
|
+
|
|
9
|
+
def _lint(self, text: str) -> list:
|
|
10
|
+
lint_file = LintFile("<memory>", text)
|
|
11
|
+
return [
|
|
12
|
+
result
|
|
13
|
+
for result in lint_file.lint(DEFAULT)
|
|
14
|
+
if result.check_result.check_path.startswith(self.RELEVANT_PREFIX)
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
def _extract_edits(self, results: list) -> list[tuple[int, int, str | None]]:
|
|
18
|
+
return sorted(
|
|
19
|
+
(
|
|
20
|
+
(
|
|
21
|
+
result.check_result.span[0] - 1,
|
|
22
|
+
result.check_result.span[1] - 1,
|
|
23
|
+
result.check_result.replacements,
|
|
24
|
+
)
|
|
25
|
+
for result in results
|
|
26
|
+
),
|
|
27
|
+
key=lambda edit: (edit[0], -(edit[1] - edit[0])),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def _resolve_overlaps(
|
|
31
|
+
self, edits: list[tuple[int, int, str | None]]
|
|
32
|
+
) -> list[tuple[int, int, str | None]]:
|
|
33
|
+
resolved: list[tuple[int, int, str | None]] = []
|
|
34
|
+
last_end = -1
|
|
35
|
+
for start, end, replacement in edits:
|
|
36
|
+
if start < last_end:
|
|
37
|
+
continue
|
|
38
|
+
resolved.append((start, end, replacement))
|
|
39
|
+
last_end = end
|
|
40
|
+
return resolved
|
|
41
|
+
|
|
42
|
+
def _apply_edits(
|
|
43
|
+
self, text: str, edits: list[tuple[int, int, str | None]]
|
|
44
|
+
) -> str:
|
|
45
|
+
cleaned = text
|
|
46
|
+
for start, end, replacement in reversed(edits):
|
|
47
|
+
cleaned = cleaned[:start] + (replacement or "") + cleaned[end:]
|
|
48
|
+
return cleaned
|
|
49
|
+
|
|
50
|
+
def remove(self, text: str) -> str:
|
|
51
|
+
results = self._lint(text)
|
|
52
|
+
edits = self._extract_edits(results)
|
|
53
|
+
resolved_edits = self._resolve_overlaps(edits)
|
|
54
|
+
return self._apply_edits(text, resolved_edits)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pal.compression.utils.spacy_setup import SpacySetup
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Interjections:
|
|
7
|
+
def __init__(self, spacy_setup: SpacySetup | None = None) -> None:
|
|
8
|
+
self.spacy_setup = spacy_setup or SpacySetup()
|
|
9
|
+
self.nlp = self.spacy_setup.load_model()
|
|
10
|
+
|
|
11
|
+
def _tokenize(self, text: str):
|
|
12
|
+
return self.nlp(text)
|
|
13
|
+
|
|
14
|
+
def _is_interjection(self, token) -> bool:
|
|
15
|
+
return token.pos_ == "INTJ"
|
|
16
|
+
|
|
17
|
+
def remove(self, text: str) -> str:
|
|
18
|
+
doc = self._tokenize(text)
|
|
19
|
+
kept_tokens = [token.text for token in doc if not self._is_interjection(token)]
|
|
20
|
+
return " ".join(kept_tokens)
|