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/terminal/decide.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Decisions: one question with a few choices, asked the way Claude asks, in Pal's colours.
|
|
2
|
+
|
|
3
|
+
┌ Database ──────────────────────────────────────────────────────────────
|
|
4
|
+
│ Which database should the app use?
|
|
5
|
+
│
|
|
6
|
+
│ ❯ 1. SQLite (Recommended)
|
|
7
|
+
│ one file, no server: right for a small app
|
|
8
|
+
│ 2. PostgreSQL
|
|
9
|
+
│ a server; better for many users at once
|
|
10
|
+
│ 3. Something else… (type it)
|
|
11
|
+
└ ↑↓ choose · enter select · 1-9 jump · esc skip
|
|
12
|
+
|
|
13
|
+
Used when the Ask agent needs a choice from the person to answer well (Build and Plan never stop to ask). Without a terminal (pipes, tests) it falls back to a numbered list and a typed answer.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import re
|
|
19
|
+
import sys
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
|
|
23
|
+
from rich.console import Console
|
|
24
|
+
from rich.text import Text
|
|
25
|
+
|
|
26
|
+
from pal.terminal import theme
|
|
27
|
+
|
|
28
|
+
OTHER = "Something else…"
|
|
29
|
+
MAX_QUESTIONS = 3
|
|
30
|
+
MAX_OPTIONS = 5
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class Option:
|
|
35
|
+
label: str
|
|
36
|
+
description: str = ""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class Question:
|
|
41
|
+
question: str
|
|
42
|
+
options: list[Option] = field(default_factory=list)
|
|
43
|
+
header: str = "Decision"
|
|
44
|
+
recommended: int | None = None
|
|
45
|
+
other: bool = True # a last "Something else…" choice that takes typed text
|
|
46
|
+
|
|
47
|
+
def labels(self) -> list[str]:
|
|
48
|
+
return [o.label + (" (Recommended)" if i == self.recommended else "") for i, o in enumerate(self.options)]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# -- what the agents send --------------------------------------------------------------------------------------------------------
|
|
52
|
+
DECIDE = ("Questions back (Ask agent only): when a good answer depends on a choice only the person can make (which of several meanings they intend, "
|
|
53
|
+
"their goal, a trade-off they must pick), do not guess: ask. End your reply with a line `### QUESTION` and a ```json block: "
|
|
54
|
+
"{\"header\": \"a 1-2 word topic\", \"question\": \"...?\", \"options\": [{\"label\": \"1-5 words\", \"description\": \"one line on what it "
|
|
55
|
+
"leads to\"}], \"recommended\": 0} with 2 to 4 options, the one you recommend first (or a JSON list of up to 3 such questions). "
|
|
56
|
+
"Ask only when it really matters; otherwise answer directly.")
|
|
57
|
+
|
|
58
|
+
_BLOCK = re.compile(r"^#+\s*QUESTIONS?\s*$\s*```(?:json)?\s*\n(.*?)```", re.M | re.S | re.I)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def parse_questions(text: str) -> tuple[list[Question], str]:
|
|
62
|
+
"""The questions in a reply's `### QUESTION` block, and the reply without the block. A block that is not valid is left in the text."""
|
|
63
|
+
m = _BLOCK.search(text or "")
|
|
64
|
+
if not m:
|
|
65
|
+
return [], text
|
|
66
|
+
try:
|
|
67
|
+
raw = json.loads(m.group(1))
|
|
68
|
+
except ValueError:
|
|
69
|
+
return [], text
|
|
70
|
+
items = raw if isinstance(raw, list) else [raw]
|
|
71
|
+
out: list[Question] = []
|
|
72
|
+
for item in items[:MAX_QUESTIONS]:
|
|
73
|
+
if not isinstance(item, dict) or not str(item.get("question", "")).strip():
|
|
74
|
+
continue
|
|
75
|
+
opts = []
|
|
76
|
+
for o in item.get("options") or []:
|
|
77
|
+
if isinstance(o, str) and o.strip():
|
|
78
|
+
opts.append(Option(o.strip()[:80]))
|
|
79
|
+
elif isinstance(o, dict) and str(o.get("label", "")).strip():
|
|
80
|
+
opts.append(Option(str(o["label"]).strip()[:80], str(o.get("description", "")).strip()[:200]))
|
|
81
|
+
if len(opts) < 2:
|
|
82
|
+
continue
|
|
83
|
+
rec = item.get("recommended")
|
|
84
|
+
out.append(Question(str(item["question"]).strip()[:300], opts[:MAX_OPTIONS], str(item.get("header") or "Decision").strip()[:24],
|
|
85
|
+
rec if isinstance(rec, int) and 0 <= rec < len(opts[:MAX_OPTIONS]) else None))
|
|
86
|
+
if not out:
|
|
87
|
+
return [], text
|
|
88
|
+
return out, (text[:m.start()] + text[m.end():]).strip()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def answers_message(questions: list[Question], answers: list[str]) -> str:
|
|
92
|
+
"""What goes back to the agent once the person has decided."""
|
|
93
|
+
lines = [f"- {q.question} → {a}" for q, a in zip(questions, answers)]
|
|
94
|
+
return "My decisions:\n" + "\n".join(lines) + "\nGo ahead with these."
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# -- asking -------------------------------------------------------------------------------------------------------------------------
|
|
98
|
+
def _interactive() -> bool:
|
|
99
|
+
return sys.stdin.isatty() and sys.stdout.isatty()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def ask(console: Console, q: Question, reader: Callable[[str], str] | None = None) -> str | None:
|
|
103
|
+
"""The chosen label (or the typed answer for "Something else…"), or None when the person skips (Esc, Ctrl+C)."""
|
|
104
|
+
if reader is not None or not _interactive():
|
|
105
|
+
return _ask_plain(console, q, reader or (lambda prompt: console.input(prompt)))
|
|
106
|
+
return _ask_card(q)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _ask_plain(console: Console, q: Question, reader: Callable[[str], str]) -> str | None:
|
|
110
|
+
console.print(Text(f" {q.header}: {q.question}", style=f"bold {theme.WHITE}"))
|
|
111
|
+
labels = q.labels() + ([OTHER] if q.other else [])
|
|
112
|
+
for i, label in enumerate(labels, 1):
|
|
113
|
+
desc = q.options[i - 1].description if i <= len(q.options) else "type your own answer"
|
|
114
|
+
console.print(Text.assemble((f" {i}. ", theme.RED_HI), (label, theme.WHITE), (f" {desc}" if desc else "", theme.MUTED)))
|
|
115
|
+
try:
|
|
116
|
+
raw = reader(" choose a number (Enter: " + (str((q.recommended or 0) + 1)) + "): ").strip()
|
|
117
|
+
except (EOFError, KeyboardInterrupt):
|
|
118
|
+
return None
|
|
119
|
+
if not raw:
|
|
120
|
+
return q.options[q.recommended or 0].label
|
|
121
|
+
if raw.isdigit() and 1 <= int(raw) <= len(labels):
|
|
122
|
+
n = int(raw) - 1
|
|
123
|
+
if n < len(q.options):
|
|
124
|
+
return q.options[n].label
|
|
125
|
+
try:
|
|
126
|
+
typed = reader(" your answer: ").strip()
|
|
127
|
+
except (EOFError, KeyboardInterrupt):
|
|
128
|
+
return None
|
|
129
|
+
return typed or None
|
|
130
|
+
return raw # typed an answer directly
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _ask_card(q: Question) -> str | None:
|
|
134
|
+
from prompt_toolkit import Application
|
|
135
|
+
from prompt_toolkit.buffer import Buffer
|
|
136
|
+
from prompt_toolkit.filters import Condition
|
|
137
|
+
from prompt_toolkit.formatted_text import FormattedText
|
|
138
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
139
|
+
from prompt_toolkit.layout import HSplit, Layout, VSplit, Window
|
|
140
|
+
from prompt_toolkit.layout.containers import ConditionalContainer
|
|
141
|
+
from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
|
|
142
|
+
from prompt_toolkit.styles import Style
|
|
143
|
+
|
|
144
|
+
labels = q.labels() + ([OTHER] if q.other else [])
|
|
145
|
+
n_other = len(q.options) if q.other else -1
|
|
146
|
+
sel = {"i": q.recommended or 0}
|
|
147
|
+
out: dict[str, str | None] = {"result": None}
|
|
148
|
+
typed = Buffer(multiline=False)
|
|
149
|
+
style = Style.from_dict({"bar": theme.RED, "head": f"bold {theme.RED_HI}", "q": f"bold {theme.WHITE}", "sel": f"bold {theme.WHITE}",
|
|
150
|
+
"pointer": f"bold {theme.RED}", "opt": theme.INK, "desc": theme.MUTED, "hint": theme.MUTED, "key": f"bold {theme.WHITE}"})
|
|
151
|
+
|
|
152
|
+
def body():
|
|
153
|
+
out = [("class:bar", " ┌ "), ("class:head", q.header), ("class:bar", " " + "─" * max(4, 60 - len(q.header)) + "\n"),
|
|
154
|
+
("class:bar", " │ "), ("class:q", q.question + "\n"), ("class:bar", " │\n")]
|
|
155
|
+
for i, label in enumerate(labels):
|
|
156
|
+
on = i == sel["i"]
|
|
157
|
+
out += [("class:bar", " │ "), ("class:pointer", "❯ " if on else " "), ("class:sel" if on else "class:opt", f"{i + 1}. {label}")]
|
|
158
|
+
if i == n_other:
|
|
159
|
+
out.append(("class:desc", " (type it below)" if not on else " type, then enter"))
|
|
160
|
+
out.append(("", "\n"))
|
|
161
|
+
desc = q.options[i].description if i < len(q.options) else ""
|
|
162
|
+
if desc:
|
|
163
|
+
out += [("class:bar", " │ "), ("class:desc", f" {desc}\n")]
|
|
164
|
+
return FormattedText(out)
|
|
165
|
+
|
|
166
|
+
def hints():
|
|
167
|
+
return FormattedText([("class:bar", " └ "), ("class:key", "↑↓"), ("class:hint", " choose · "), ("class:key", "enter"), ("class:hint", " select · "),
|
|
168
|
+
("class:key", "1-9"), ("class:hint", " jump · "), ("class:key", "esc"), ("class:hint", " skip")])
|
|
169
|
+
kb = KeyBindings()
|
|
170
|
+
typing = Condition(lambda: sel["i"] == n_other)
|
|
171
|
+
|
|
172
|
+
@kb.add("up")
|
|
173
|
+
def _(e):
|
|
174
|
+
sel["i"] = (sel["i"] - 1) % len(labels)
|
|
175
|
+
|
|
176
|
+
@kb.add("down")
|
|
177
|
+
@kb.add("tab")
|
|
178
|
+
def _(e):
|
|
179
|
+
sel["i"] = (sel["i"] + 1) % len(labels)
|
|
180
|
+
|
|
181
|
+
for d in range(1, min(9, len(labels)) + 1):
|
|
182
|
+
@kb.add(str(d), filter=~typing | Condition(lambda: not typed.text))
|
|
183
|
+
def _(e, d=d):
|
|
184
|
+
sel["i"] = d - 1
|
|
185
|
+
if d - 1 != n_other:
|
|
186
|
+
out["result"] = q.options[d - 1].label
|
|
187
|
+
e.app.exit()
|
|
188
|
+
|
|
189
|
+
@kb.add("enter")
|
|
190
|
+
def _(e):
|
|
191
|
+
if sel["i"] == n_other:
|
|
192
|
+
if typed.text.strip():
|
|
193
|
+
out["result"] = typed.text.strip()
|
|
194
|
+
e.app.exit()
|
|
195
|
+
return
|
|
196
|
+
out["result"] = q.options[sel["i"]].label
|
|
197
|
+
e.app.exit()
|
|
198
|
+
|
|
199
|
+
@kb.add("escape", eager=True)
|
|
200
|
+
@kb.add("c-c")
|
|
201
|
+
def _(e):
|
|
202
|
+
e.app.exit()
|
|
203
|
+
typed_win = Window(BufferControl(typed), height=1)
|
|
204
|
+
field_row = ConditionalContainer(VSplit([Window(FormattedTextControl([("class:bar", " │ "), ("class:pointer", "› ")]), width=9, dont_extend_width=True),
|
|
205
|
+
typed_win]), filter=typing)
|
|
206
|
+
if n_other >= 0: # typing anything goes to "Something else…"
|
|
207
|
+
typed.on_text_changed += lambda _b: sel.update(i=n_other) if typed.text else None
|
|
208
|
+
root = HSplit([Window(FormattedTextControl(body), dont_extend_height=True), field_row, Window(FormattedTextControl(hints), height=1)])
|
|
209
|
+
app: Application = Application(layout=Layout(root, focused_element=typed_win), key_bindings=kb, style=style, full_screen=False, erase_when_done=True)
|
|
210
|
+
try:
|
|
211
|
+
app.run()
|
|
212
|
+
except (EOFError, KeyboardInterrupt):
|
|
213
|
+
return None
|
|
214
|
+
return out["result"]
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def show_answer(console: Console, q: Question, answer: str | None) -> None:
|
|
218
|
+
"""What stays on screen after the card disappears: the question and what was chosen."""
|
|
219
|
+
console.print(Text.assemble((" ◆ ", theme.RED), (f"{q.question} ", theme.INK), ("→ " + answer if answer else "→ skipped", f"bold {theme.WHITE}" if answer else theme.MUTED)))
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def ask_all(console: Console, questions: list[Question], reader: Callable[[str], str] | None = None) -> list[str] | None:
|
|
223
|
+
"""Every question in turn. None when the person skips one."""
|
|
224
|
+
answers = []
|
|
225
|
+
for q in questions:
|
|
226
|
+
a = ask(console, q, reader)
|
|
227
|
+
show_answer(console, q, a)
|
|
228
|
+
if a is None:
|
|
229
|
+
return None
|
|
230
|
+
answers.append(a)
|
|
231
|
+
return answers
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def yes_no(console: Console, question: str, yes: str = "Yes", no: str = "No", yes_about: str = "", no_about: str = "", default: bool = False,
|
|
235
|
+
header: str = "Decision", reader: Callable[[str], str] | None = None) -> bool:
|
|
236
|
+
"""A yes/no decision as a card. The default is the recommended choice."""
|
|
237
|
+
q = Question(question, [Option(yes, yes_about), Option(no, no_about)], header, 0 if default else 1, other=False)
|
|
238
|
+
if reader is None and not _interactive():
|
|
239
|
+
return default
|
|
240
|
+
a = ask(console, q, reader)
|
|
241
|
+
show_answer(console, q, a)
|
|
242
|
+
return a == yes
|
pal/terminal/files.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Reading a model's reply: which files did it write? Tolerant of the many ways models label a code block."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
_FENCE = re.compile(r"```(?P<info>[^\n`]*)\n(?P<body>.*?)\n?```", re.S)
|
|
8
|
+
_NAME = re.compile(r"(?<![\w.-])((?:[A-Za-z]:)?[/\\]?(?:(?:\.\.|[\w-]+)[/\\])*[\w-][\w.-]*\.[A-Za-z0-9]{1,5})(?![\w/\\-])")
|
|
9
|
+
_INFO = re.compile(r"(?:title|filename|file|path)\s*=\s*[\"']?([^\s\"']+)", re.I)
|
|
10
|
+
_FIRST_LINE = re.compile(r"^\s*(?:<!--|//|#|/\*)\s*(?:file(?:name)?|path)\s*:\s*(\S+?)\s*(?:-->|\*/)?\s*$", re.I)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ParsedFile:
|
|
15
|
+
path: str
|
|
16
|
+
content: str
|
|
17
|
+
how: str = ""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _hint(text_before: str) -> str:
|
|
21
|
+
"""A file name in the last non-empty line before a code block, or ''."""
|
|
22
|
+
for line in reversed(text_before.splitlines()):
|
|
23
|
+
if line.strip():
|
|
24
|
+
names = _NAME.findall(line)
|
|
25
|
+
return names[-1] if names else ""
|
|
26
|
+
return ""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def parse_files(reply: str, default_name: str = "") -> tuple[list[ParsedFile], int]:
|
|
30
|
+
"""(the files in the reply, how many code blocks had no usable name). The name comes from the line before the block ("### FILE: index.html",
|
|
31
|
+
"**style.css**", "`app.py`"), from the block's info string (title="x"), or from a first-line comment (<!-- file: x -->). A reply with exactly one
|
|
32
|
+
unnamed block uses `default_name`, if there is one."""
|
|
33
|
+
found: list[ParsedFile] = []
|
|
34
|
+
unnamed = 0
|
|
35
|
+
blocks = list(_FENCE.finditer(reply))
|
|
36
|
+
for i, m in enumerate(blocks):
|
|
37
|
+
before = reply[(blocks[i - 1].end() if i else 0):m.start()]
|
|
38
|
+
body = m.group("body")
|
|
39
|
+
name, how = "", ""
|
|
40
|
+
info = _INFO.search(m.group("info"))
|
|
41
|
+
if info:
|
|
42
|
+
name, how = info.group(1), "info string"
|
|
43
|
+
if not name:
|
|
44
|
+
first = _FIRST_LINE.match(body.split("\n", 1)[0])
|
|
45
|
+
if first:
|
|
46
|
+
name, how, body = first.group(1), "first-line comment", body.split("\n", 1)[1] if "\n" in body else ""
|
|
47
|
+
if not name:
|
|
48
|
+
name, how = _hint(before), "line before"
|
|
49
|
+
if not name and len(blocks) == 1 and default_name:
|
|
50
|
+
name, how = default_name, "default"
|
|
51
|
+
if not name:
|
|
52
|
+
unnamed += 1
|
|
53
|
+
continue
|
|
54
|
+
found.append(ParsedFile(name.strip("`*"), body.rstrip("\n") + "\n", how))
|
|
55
|
+
return found, unnamed
|