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.
Files changed (282) hide show
  1. pal/__init__.py +29 -0
  2. pal/__main__.py +3 -0
  3. pal/access_manager/__init__.py +16 -0
  4. pal/access_manager/components/__init__.py +14 -0
  5. pal/access_manager/components/access_control.py +41 -0
  6. pal/access_manager/components/base.py +39 -0
  7. pal/access_manager/components/intervention.py +31 -0
  8. pal/access_manager/components/privilege_registry.py +30 -0
  9. pal/access_manager/pipeline.py +68 -0
  10. pal/access_manager/utils/__init__.py +2 -0
  11. pal/agent_constraints/__init__.py +3 -0
  12. pal/agent_constraints/components/AGENT_OUTPUT.md +13 -0
  13. pal/agent_constraints/components/__init__.py +3 -0
  14. pal/agent_constraints/components/constraints.py +22 -0
  15. pal/agent_constraints/pipeline.py +22 -0
  16. pal/agent_constraints/utils/__init__.py +2 -0
  17. pal/agents/__init__.py +12 -0
  18. pal/agents/coding.py +14 -0
  19. pal/agents/docs.py +15 -0
  20. pal/chat/__init__.py +4 -0
  21. pal/chat/demo.py +231 -0
  22. pal/chat/providers.py +23 -0
  23. pal/chat/service.py +591 -0
  24. pal/chat/services/__init__.py +26 -0
  25. pal/chat/services/base.py +142 -0
  26. pal/chat/services/data.py +142 -0
  27. pal/chat/services/media.py +123 -0
  28. pal/chat/services/web.py +60 -0
  29. pal/chat/sessions.py +108 -0
  30. pal/chat/tools.py +446 -0
  31. pal/chat/uploads.py +81 -0
  32. pal/cli.py +502 -0
  33. pal/compression/__init__.py +12 -0
  34. pal/compression/components/__init__.py +64 -0
  35. pal/compression/components/adverbs.py +20 -0
  36. pal/compression/components/hedges.py +54 -0
  37. pal/compression/components/interjections.py +20 -0
  38. pal/compression/components/minifier.py +56 -0
  39. pal/compression/components/output_compressor.py +13 -0
  40. pal/compression/components/pleonasms.py +54 -0
  41. pal/compression/components/punctuations.py +36 -0
  42. pal/compression/components/stopwords.py +62 -0
  43. pal/compression/history.py +346 -0
  44. pal/compression/pipeline.py +58 -0
  45. pal/compression/utils/__init__.py +30 -0
  46. pal/compression/utils/negations.py +18 -0
  47. pal/compression/utils/nltk_setup.py +20 -0
  48. pal/compression/utils/pronouns.py +27 -0
  49. pal/compression/utils/proselint_setup.py +9 -0
  50. pal/compression/utils/spacy_setup.py +22 -0
  51. pal/config/default.yaml +163 -0
  52. pal/config.py +504 -0
  53. pal/context_manager/__init__.py +7 -0
  54. pal/context_manager/chat_context.py +117 -0
  55. pal/context_manager/components/__init__.py +5 -0
  56. pal/context_manager/components/assembler.py +75 -0
  57. pal/context_manager/components/context_cache.py +45 -0
  58. pal/context_manager/components/kv_bridge.py +16 -0
  59. pal/context_manager/components/pager.py +48 -0
  60. pal/context_manager/components/simple_context_manager.py +56 -0
  61. pal/context_manager/components/turns.py +88 -0
  62. pal/context_manager/pipeline.py +27 -0
  63. pal/context_manager/utils/__init__.py +18 -0
  64. pal/context_manager/utils/hashing.py +5 -0
  65. pal/context_manager/utils/relevance.py +59 -0
  66. pal/context_manager/utils/storage.py +37 -0
  67. pal/documents/__init__.py +4 -0
  68. pal/documents/components/__init__.py +0 -0
  69. pal/documents/components/chunker.py +79 -0
  70. pal/documents/components/index.py +155 -0
  71. pal/documents/utils/__init__.py +1 -0
  72. pal/errors.py +40 -0
  73. pal/input_converters/__init__.py +8 -0
  74. pal/input_converters/components/__init__.py +0 -0
  75. pal/input_converters/components/base.py +38 -0
  76. pal/input_converters/components/pandoc_converter.py +87 -0
  77. pal/input_converters/components/pdf_converter.py +46 -0
  78. pal/input_converters/components/pptx_converter.py +52 -0
  79. pal/input_converters/components/xlsx_converter.py +38 -0
  80. pal/input_converters/pipeline.py +72 -0
  81. pal/input_converters/utils/__init__.py +1 -0
  82. pal/input_converters/workspace.py +56 -0
  83. pal/input_savers/INPUT_RULES.md +2 -0
  84. pal/input_savers/__init__.py +5 -0
  85. pal/input_savers/components/__init__.py +0 -0
  86. pal/input_savers/components/base.py +40 -0
  87. pal/input_savers/components/methods.py +41 -0
  88. pal/input_savers/components/verifier.py +39 -0
  89. pal/input_savers/pipeline.py +154 -0
  90. pal/input_savers/utils/__init__.py +0 -0
  91. pal/input_savers/utils/guards.py +94 -0
  92. pal/knowledge_graph/__init__.py +10 -0
  93. pal/knowledge_graph/components/__init__.py +0 -0
  94. pal/knowledge_graph/components/base.py +50 -0
  95. pal/knowledge_graph/components/builder.py +54 -0
  96. pal/knowledge_graph/components/retriever.py +100 -0
  97. pal/knowledge_graph/components/workspace.py +116 -0
  98. pal/knowledge_graph/pipeline.py +215 -0
  99. pal/knowledge_graph/utils/__init__.py +1 -0
  100. pal/latency_tracker/__init__.py +3 -0
  101. pal/latency_tracker/pipeline.py +86 -0
  102. pal/llm_scheduler/__init__.py +11 -0
  103. pal/llm_scheduler/api.py +137 -0
  104. pal/llm_scheduler/code.py +28 -0
  105. pal/llm_scheduler/core.py +92 -0
  106. pal/llm_scheduler/local.py +215 -0
  107. pal/llm_scheduler/pipeline.py +42 -0
  108. pal/llm_scheduler/types.py +92 -0
  109. pal/llm_wrapper/__init__.py +34 -0
  110. pal/llm_wrapper/adapter.py +316 -0
  111. pal/llm_wrapper/components/__init__.py +23 -0
  112. pal/llm_wrapper/components/adapter_types.py +90 -0
  113. pal/llm_wrapper/components/base.py +195 -0
  114. pal/llm_wrapper/components/features.py +104 -0
  115. pal/llm_wrapper/components/litellm_wrapper.py +215 -0
  116. pal/llm_wrapper/components/openrouter_wrapper.py +114 -0
  117. pal/llm_wrapper/modes.py +73 -0
  118. pal/llm_wrapper/pipeline.py +61 -0
  119. pal/llm_wrapper/utils/__init__.py +4 -0
  120. pal/llm_wrapper/utils/registry.py +49 -0
  121. pal/llm_wrapper/utils/token_counting.py +10 -0
  122. pal/local_llm/__init__.py +5 -0
  123. pal/local_llm/components/__init__.py +0 -0
  124. pal/local_llm/components/huggingface_local.py +217 -0
  125. pal/local_llm/components/local_llm.py +60 -0
  126. pal/logging_setup.py +18 -0
  127. pal/memory/__init__.py +7 -0
  128. pal/memory/components/__init__.py +6 -0
  129. pal/memory/components/lru_k.py +56 -0
  130. pal/memory/components/manager.py +98 -0
  131. pal/memory/components/note.py +54 -0
  132. pal/memory/components/storage_manager.py +60 -0
  133. pal/memory/pipeline.py +122 -0
  134. pal/memory/utils/__init__.py +3 -0
  135. pal/memory/utils/keyword_overlap.py +3 -0
  136. pal/memory_tracker/__init__.py +3 -0
  137. pal/memory_tracker/components/__init__.py +0 -0
  138. pal/memory_tracker/components/process.py +56 -0
  139. pal/memory_tracker/pipeline.py +44 -0
  140. pal/metrics/__init__.py +4 -0
  141. pal/metrics/components/__init__.py +4 -0
  142. pal/metrics/components/collector.py +140 -0
  143. pal/metrics/components/exporter.py +28 -0
  144. pal/metrics/pipeline.py +49 -0
  145. pal/metrics/utils/__init__.py +2 -0
  146. pal/notes_manager/__init__.py +5 -0
  147. pal/notes_manager/components/__init__.py +0 -0
  148. pal/notes_manager/components/privacy.py +14 -0
  149. pal/notes_manager/components/recall.py +31 -0
  150. pal/notes_manager/components/tidy.py +28 -0
  151. pal/notes_manager/pipeline.py +138 -0
  152. pal/orchestrator/__init__.py +26 -0
  153. pal/orchestrator/components/__init__.py +26 -0
  154. pal/orchestrator/components/agent.py +105 -0
  155. pal/orchestrator/components/cgroup_manager.py +209 -0
  156. pal/orchestrator/components/context_interrupt.py +220 -0
  157. pal/orchestrator/components/limits.py +9 -0
  158. pal/orchestrator/components/scheduler.py +69 -0
  159. pal/orchestrator/components/syscall_scheduler.py +200 -0
  160. pal/orchestrator/pipeline.py +228 -0
  161. pal/orchestrator/utils/__init__.py +4 -0
  162. pal/orchestrator/utils/device.py +164 -0
  163. pal/orchestrator/utils/protocol.py +32 -0
  164. pal/os_kernel/__init__.py +5 -0
  165. pal/os_kernel/filesystem.py +80 -0
  166. pal/os_kernel/network.py +146 -0
  167. pal/os_kernel/pipeline.py +175 -0
  168. pal/os_kernel/sysinfo.py +36 -0
  169. pal/paths.py +59 -0
  170. pal/pipeline.py +177 -0
  171. pal/py.typed +0 -0
  172. pal/redact.py +19 -0
  173. pal/sdk/__init__.py +33 -0
  174. pal/sdk/adapters.py +43 -0
  175. pal/sdk/agent.py +99 -0
  176. pal/sdk/components/__init__.py +14 -0
  177. pal/sdk/components/autogen_adapter.py +65 -0
  178. pal/sdk/components/autogen_client.py +98 -0
  179. pal/sdk/components/framework_adapter_base.py +69 -0
  180. pal/sdk/components/metagpt_adapter.py +46 -0
  181. pal/sdk/components/open_interpreter_adapter.py +47 -0
  182. pal/sdk/components/tool_catalog.py +44 -0
  183. pal/sdk/os_api.py +84 -0
  184. pal/sdk/patterns.py +129 -0
  185. pal/sdk/pipeline.py +240 -0
  186. pal/sdk/queries.py +68 -0
  187. pal/sdk/savers.py +88 -0
  188. pal/sdk/utils/__init__.py +2 -0
  189. pal/storage_manager/__init__.py +17 -0
  190. pal/storage_manager/components/__init__.py +19 -0
  191. pal/storage_manager/components/base.py +50 -0
  192. pal/storage_manager/components/local_file_backend.py +84 -0
  193. pal/storage_manager/components/storage_queue.py +91 -0
  194. pal/storage_manager/components/vector_store_backend.py +122 -0
  195. pal/storage_manager/pipeline.py +153 -0
  196. pal/storage_manager/utils/__init__.py +4 -0
  197. pal/storage_manager/utils/memory_adapter.py +60 -0
  198. pal/storage_manager/utils/sdk.py +93 -0
  199. pal/studio/__init__.py +0 -0
  200. pal/studio/builder.py +166 -0
  201. pal/studio/catalog.py +90 -0
  202. pal/studio/checks.py +129 -0
  203. pal/studio/clients.py +6 -0
  204. pal/studio/editor.py +137 -0
  205. pal/studio/files.py +6 -0
  206. pal/studio/models.py +6 -0
  207. pal/studio/plan.py +149 -0
  208. pal/studio/planner.py +205 -0
  209. pal/studio/policy.py +69 -0
  210. pal/studio/routes.py +254 -0
  211. pal/studio/runs.py +210 -0
  212. pal/task_router/__init__.py +11 -0
  213. pal/task_router/components/__init__.py +0 -0
  214. pal/task_router/components/answerer.py +37 -0
  215. pal/task_router/components/base.py +34 -0
  216. pal/task_router/components/classifier.py +40 -0
  217. pal/task_router/components/gates.py +23 -0
  218. pal/task_router/pipeline.py +61 -0
  219. pal/task_router/utils/__init__.py +1 -0
  220. pal/terminal/__init__.py +1 -0
  221. pal/terminal/backend.py +141 -0
  222. pal/terminal/bench.py +173 -0
  223. pal/terminal/cli.py +826 -0
  224. pal/terminal/clients.py +170 -0
  225. pal/terminal/commands.py +401 -0
  226. pal/terminal/decide.py +242 -0
  227. pal/terminal/files.py +55 -0
  228. pal/terminal/flow_cli.py +439 -0
  229. pal/terminal/hardware.py +199 -0
  230. pal/terminal/history.py +88 -0
  231. pal/terminal/keys.py +83 -0
  232. pal/terminal/local_models.py +261 -0
  233. pal/terminal/logo.py +117 -0
  234. pal/terminal/models.py +99 -0
  235. pal/terminal/policy.py +54 -0
  236. pal/terminal/project.py +413 -0
  237. pal/terminal/review.py +65 -0
  238. pal/terminal/security.py +239 -0
  239. pal/terminal/session.py +503 -0
  240. pal/terminal/setup.py +354 -0
  241. pal/terminal/syntax.py +98 -0
  242. pal/terminal/theme.py +37 -0
  243. pal/terminal/ui.py +647 -0
  244. pal/terminal/voice.py +137 -0
  245. pal/terminal/workflow.py +624 -0
  246. pal/token_tracker/__init__.py +3 -0
  247. pal/token_tracker/pipeline.py +54 -0
  248. pal/tokens.py +21 -0
  249. pal/tool_catalog.py +107 -0
  250. pal/tool_manager/__init__.py +17 -0
  251. pal/tool_manager/builtin.py +82 -0
  252. pal/tool_manager/components/__init__.py +19 -0
  253. pal/tool_manager/components/base.py +53 -0
  254. pal/tool_manager/components/conflict_resolver.py +27 -0
  255. pal/tool_manager/components/param_validator.py +41 -0
  256. pal/tool_manager/components/tool_call_queue.py +111 -0
  257. pal/tool_manager/components/tool_registry.py +60 -0
  258. pal/tool_manager/pipeline.py +80 -0
  259. pal/tool_manager/utils/__init__.py +3 -0
  260. pal/web/__init__.py +3 -0
  261. pal/web/app.py +276 -0
  262. pal/web/static/editor.js +219 -0
  263. pal/web/static/fonts/LICENSE-Inter.txt +3 -0
  264. pal/web/static/fonts/inter-400.woff2 +0 -0
  265. pal/web/static/fonts/inter-500.woff2 +0 -0
  266. pal/web/static/fonts/inter-600.woff2 +0 -0
  267. pal/web/static/fonts/inter-700.woff2 +0 -0
  268. pal/web/static/inter.css +5 -0
  269. pal/web/static/pal-logo-256.png +0 -0
  270. pal/web/static/pal-logo-32.png +0 -0
  271. pal/web/static/pal-logo-64.png +0 -0
  272. pal/web/static/studio.css +306 -0
  273. pal/web/static/studio.js +533 -0
  274. pal/web/static/voice.js +76 -0
  275. pal/web/templates/index.html +368 -0
  276. pal/web/templates/studio.html +120 -0
  277. pal_code-0.1.0.dist-info/METADATA +379 -0
  278. pal_code-0.1.0.dist-info/RECORD +282 -0
  279. pal_code-0.1.0.dist-info/WHEEL +5 -0
  280. pal_code-0.1.0.dist-info/entry_points.txt +2 -0
  281. pal_code-0.1.0.dist-info/licenses/LICENSE +21 -0
  282. pal_code-0.1.0.dist-info/top_level.txt +1 -0
pal/web/app.py ADDED
@@ -0,0 +1,276 @@
1
+ """HTTP layer: a thin Flask app around ChatService. No business logic lives here.
2
+
3
+ - create_app() is a factory, so tests and servers build their own app around their own service.
4
+ - Errors: PalError subclasses map to their HTTP status with a safe message; anything else is
5
+ logged with a traceback and answered with a generic 500 (no traceback ever reaches the client).
6
+ - API keys arrive per request and are passed straight through; they are never logged or stored.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import logging
12
+ import os
13
+ import re
14
+ import uuid
15
+ from dataclasses import asdict
16
+ from typing import Any, Optional
17
+
18
+ from flask import Flask, Response, jsonify, render_template, request
19
+ from werkzeug.exceptions import HTTPException
20
+
21
+ from pal.chat import API_PROVIDERS, ChatService
22
+ from pal.chat.demo import Demo
23
+ from pal.chat.sessions import validate_session_id
24
+ from pal.chat.uploads import ( # noqa: F401 (read_upload is part of this module's API)
25
+ MEDIA_TYPES,
26
+ _clean_filename,
27
+ _is_text,
28
+ read_upload,
29
+ )
30
+ from pal.config import Settings
31
+ from pal.errors import NotFoundError, PalError, UserInputError
32
+ from pal.input_savers.components.methods import METHODS
33
+ from pal.llm_wrapper.modes import default_mode, describe_modes
34
+ from pal.sdk.agent import list_agents
35
+
36
+ log = logging.getLogger(__name__)
37
+
38
+ _SECURITY_HEADERS = {
39
+ "X-Content-Type-Options": "nosniff",
40
+ "X-Frame-Options": "DENY",
41
+ "Referrer-Policy": "no-referrer",
42
+ "Cache-Control": "no-store",
43
+ }
44
+
45
+
46
+ def _saver_json(res) -> Optional[dict]:
47
+ if res is None: # turn handled without an API call: no input saver ran
48
+ return None
49
+ return {
50
+ "original": res.original, "reduced": res.reduced,
51
+ "tokens_original": res.tokens_original, "tokens_reduced": res.tokens_reduced,
52
+ "tokens_saved": res.tokens_saved,
53
+ "steps": [asdict(s) for s in res.steps],
54
+ }
55
+
56
+
57
+ def _flag(v: Any) -> bool:
58
+ return v is True or str(v).lower() in ("1", "true", "on", "yes")
59
+
60
+
61
+ def _pasted(v: Any) -> list:
62
+ """Pasted spans the client tracked; the LLM never sees or edits them."""
63
+ if isinstance(v, list):
64
+ return [x for x in v if isinstance(x, str)]
65
+ try:
66
+ out = json.loads(v or "[]")
67
+ except ValueError:
68
+ return []
69
+ return [x for x in out if isinstance(x, str)] if isinstance(out, list) else []
70
+
71
+
72
+ def create_app(service: Optional[ChatService] = None, settings: Optional[Settings] = None, studio: Optional[dict] = None) -> Flask:
73
+ """`studio` passes options to the Studio (its planner model, builder model and model discovery); tests use it to plug in fakes."""
74
+ service = service or ChatService(settings)
75
+ cfg = service.settings
76
+ app = Flask(__name__)
77
+ app.config["MAX_CONTENT_LENGTH"] = cfg.server.max_upload_mb * 1024 * 1024
78
+ app.extensions["pal_service"] = service
79
+ demo = Demo(service)
80
+
81
+ @app.after_request
82
+ def _headers(resp: Response) -> Response:
83
+ for k, v in _SECURITY_HEADERS.items():
84
+ resp.headers.setdefault(k, v)
85
+ return resp
86
+
87
+ @app.errorhandler(PalError)
88
+ def _pal_error(e: PalError):
89
+ if e.http_status >= 500:
90
+ log.error("request failed: %s: %s", type(e).__name__, e)
91
+ msg = str(e) if e.public else "internal error"
92
+ return jsonify({"error": msg, "code": type(e).__name__}), e.http_status
93
+
94
+ @app.errorhandler(HTTPException)
95
+ def _http_error(e: HTTPException):
96
+ msg = "upload too large" if e.code == 413 else (e.description or e.name)
97
+ return jsonify({"error": msg, "code": e.name.replace(" ", "")}), e.code or 500
98
+
99
+ @app.errorhandler(Exception)
100
+ def _unexpected(e: Exception):
101
+ log.exception("unhandled error")
102
+ return jsonify({"error": "internal error", "code": "InternalError"}), 500
103
+
104
+ @app.get("/")
105
+ def index():
106
+ return render_template("index.html")
107
+
108
+ @app.get("/healthz")
109
+ def healthz():
110
+ return jsonify({"ok": True})
111
+
112
+ @app.get("/api/config")
113
+ def config():
114
+ return jsonify({
115
+ "providers": {p: bool(os.environ.get(env)) for p, env in API_PROVIDERS.items()},
116
+ "llm_modes": describe_modes(), "llm_mode": default_mode(),
117
+ "methods": [{"id": m, "name": METHODS[m].name} for m in cfg.input_methods],
118
+ "local_llm": {k: v for k, v in cfg.local_llm.items() if k != "api_key"},
119
+ "mode": cfg.input_mode,
120
+ "routing": {"enabled": cfg.task_routing.enabled, "default": cfg.task_routing.default},
121
+ "graph": {"enabled": service.graph.enabled, "available": service.graph.available()},
122
+ "memory": {"enabled": cfg.memory.enabled},
123
+ "agents": list_agents(service.agents) if cfg.agents.enabled else [],
124
+ "metrics": {"enabled": cfg.metrics.enabled},
125
+ })
126
+
127
+ @app.get("/api/llm/modes")
128
+ def llm_modes():
129
+ return jsonify({"default": default_mode(), "modes": describe_modes()})
130
+
131
+ @app.get("/api/llm/models")
132
+ def llm_models():
133
+ """OpenRouter's models for the Explore mode: ?q=words&free=1&limit=50."""
134
+ try:
135
+ limit = max(1, min(int(request.args.get("limit", "50")), 500))
136
+ except ValueError:
137
+ raise UserInputError("limit must be a number") from None
138
+ free = request.args.get("free", "").lower() in ("1", "true", "yes")
139
+ return jsonify({"models": service.model_catalogue(request.args.get("q", ""), free, limit)})
140
+
141
+ @app.get("/api/voice")
142
+ def voice_status():
143
+ """Which speech-to-text engines this server has: the page records and sends the audio here when there is one."""
144
+ from pal.terminal import voice
145
+ return jsonify({"engines": voice.engines()})
146
+
147
+ @app.post("/api/transcribe")
148
+ def transcribe():
149
+ """The recorded audio (the request body, as the browser's MediaRecorder made it) -> text. Nothing is kept."""
150
+ from pal.terminal import voice
151
+ audio = request.get_data(cache=False)
152
+ if not audio:
153
+ raise UserInputError("no audio received")
154
+ kind = (request.content_type or "").split(";")[0]
155
+ suffix = {"audio/webm": ".webm", "audio/ogg": ".ogg", "audio/mp4": ".m4a", "audio/mpeg": ".mp3", "audio/wav": ".wav", "audio/x-wav": ".wav"}.get(kind, ".webm")
156
+ try:
157
+ heard = voice.transcribe(audio, suffix)
158
+ except voice.VoiceUnavailable as e:
159
+ raise UserInputError(str(e)) from None
160
+ return jsonify({"text": heard.text, "engine": heard.engine})
161
+
162
+ @app.get("/api/local_status")
163
+ def local_status():
164
+ return jsonify(service.local_status())
165
+
166
+ @app.post("/api/chat")
167
+ def chat():
168
+ d: dict[str, Any]
169
+ if request.is_json:
170
+ d, files = request.get_json(silent=True) or {}, []
171
+ else:
172
+ d = dict(request.form.to_dict())
173
+ if "methods" in request.form:
174
+ d["methods"] = request.form.getlist("methods")
175
+ files = request.files.getlist("files")
176
+ sid = d.get("session_id") or uuid.uuid4().hex
177
+ attachments, errors = [], []
178
+ for f in files:
179
+ try:
180
+ attachments.append(read_upload(service, f, sid))
181
+ except UserInputError as e:
182
+ errors.append(str(e))
183
+ message = d.get("message") or ""
184
+ if errors and not (message.strip() or attachments):
185
+ return jsonify({"error": "; ".join(errors)}), 400
186
+ res = service.send(
187
+ sid, d.get("provider", ""), d.get("model", ""), d.get("api_key") or None, message, attachments,
188
+ d.get("methods"), d.get("mode") or None, _pasted(d.get("pasted")), d.get("route") or None,
189
+ agent=d.get("agent") or None)
190
+ return jsonify({
191
+ "reply": res.reply, "sent": res.sent,
192
+ "input_tokens": res.input_tokens, "output_tokens": res.output_tokens,
193
+ "saver": _saver_json(res.saver), "file_errors": errors,
194
+ "handled_by": res.handled_by, "route_reason": res.route_reason,
195
+ "graph": res.graph, "documents": res.documents, "memory": res.memory,
196
+ "context": res.context, "tools": res.tools, "latency": res.latency, "fallback": res.fallback,
197
+ "cost_usd": res.cost_usd, "files": res.files,
198
+ })
199
+
200
+ @app.post("/api/experiment")
201
+ def experiment():
202
+ d = request.get_json(silent=True) or {}
203
+ if not (d.get("text") or "").strip():
204
+ raise UserInputError("enter some text")
205
+ out = service.experiment(
206
+ d["text"], d.get("methods"), d.get("provider", ""), d.get("model", ""), d.get("api_key") or None,
207
+ _flag(d.get("call_api")), d.get("mode") or None, _pasted(d.get("pasted")))
208
+ return jsonify({"saver": _saver_json(out["saver"]), "counter": out["counter"], "api": out["api"]})
209
+
210
+ @app.post("/api/reset")
211
+ def reset():
212
+ service.reset((request.get_json(silent=True) or {}).get("session_id", ""))
213
+ return jsonify({"ok": True})
214
+
215
+ @app.get("/api/workspace")
216
+ def workspace_list():
217
+ """Files an agent saved for this session (its workspace in the OS kernel interface)."""
218
+ sess = _session_or_404(request.args.get("session_id", ""))
219
+ return jsonify({"files": service.os_kernel.workspace(sess.agent_id).list()})
220
+
221
+ @app.get("/api/workspace/file")
222
+ def workspace_file():
223
+ sess = _session_or_404(request.args.get("session_id", ""))
224
+ path = request.args.get("path", "")
225
+ data = service.os_kernel.workspace(sess.agent_id).read_bytes(path) # validated: inside this session's workspace only
226
+ ext = "." + path.rsplit(".", 1)[1].lower() if "." in path else ""
227
+ mime = MEDIA_TYPES.get(ext, "text/plain" if _is_text(data) else "application/octet-stream")
228
+ name = re.sub(r"[^A-Za-z0-9._-]", "_", path.rsplit("/", 1)[-1]) or "file.txt"
229
+ return Response(data, mimetype=mime, headers={"Content-Disposition": f'attachment; filename="{name}"'})
230
+
231
+ def _session_or_404(session_id: str):
232
+ validate_session_id(session_id) # a malformed id is a 400
233
+ sess = service.sessions.get(session_id)
234
+ if sess is None:
235
+ raise NotFoundError("no such session")
236
+ return sess
237
+
238
+ @app.get("/api/metrics")
239
+ def metrics():
240
+ sid = request.args.get("session") or None
241
+ return jsonify({"summary": service.metrics.chat_summary(sid), "recent": service.metrics.turns()[-30:],
242
+ "scheduler": service.scheduler.snapshot(), "memory": service.metrics.memory.snapshot()})
243
+
244
+ @app.post("/api/metrics/reset")
245
+ def metrics_reset():
246
+ service.metrics.reset()
247
+ return jsonify({"ok": True})
248
+
249
+ @app.post("/api/demo/start")
250
+ def demo_start():
251
+ d = request.get_json(silent=True) or {}
252
+ return jsonify(demo.start(d.get("provider", ""), d.get("model", ""), d.get("api_key") or None))
253
+
254
+ @app.post("/api/demo/step")
255
+ def demo_step():
256
+ d = request.get_json(silent=True) or {}
257
+ return jsonify(demo.step(d.get("run_id", ""), d.get("api_key") or None))
258
+
259
+ @app.post("/api/demo/cancel")
260
+ def demo_cancel():
261
+ demo.cancel((request.get_json(silent=True) or {}).get("run_id", ""))
262
+ return jsonify({"ok": True})
263
+
264
+ from pal.studio.routes import register_studio
265
+ register_studio(app, service, **(studio or {})) # the Studio: describe a product, review the plan, watch it being built
266
+
267
+ @app.get("/api/memory")
268
+ def memory_status():
269
+ return jsonify({"enabled": cfg.memory.enabled, "notes": service.memory_count() if cfg.memory.enabled else 0,
270
+ "stats": service.notes.stats() if cfg.memory.enabled else None})
271
+
272
+ @app.delete("/api/memory")
273
+ def memory_clear():
274
+ return jsonify({"cleared": service.clear_memory() if cfg.memory.enabled else 0})
275
+
276
+ return app
@@ -0,0 +1,219 @@
1
+ /* Pal editor: open a finished build as a project. File list, tabs, a code pane with line numbers, and "Ask Pal" on the right: Pal proposes a change as a diff,
2
+ you accept or reject it. Every request goes through the token-saving client on the server, and the panel shows what it cost.
3
+ Everything a model or a file contains is shown with textContent (or as a textarea's value), never as HTML. */
4
+ (() => {
5
+ 'use strict';
6
+ const $ = (id) => document.getElementById(id);
7
+ const wait = () => new Promise((r) => { const t = setInterval(() => { if (window.studio && window.studio.util) { clearInterval(t); r(window.studio); } }, 20); });
8
+
9
+ wait().then((studio) => {
10
+ const { h, api, post, n } = studio.util;
11
+ const E = { run: null, arm: '', files: [], tabs: [], cur: '', buf: {}, proposal: null, busy: false, ctl: null, seq: 0 };
12
+
13
+ const has = () => !!E.run;
14
+ const dirty = (p) => E.buf[p] && E.buf[p].text !== E.buf[p].orig;
15
+ const say = (text, cls = '') => { const m = h('div', { class: 'edmsg ' + cls, text }); $('edmsgs').append(m); $('edmsgs').scrollTop = $('edmsgs').scrollHeight; return m; };
16
+ const status = (t, bad = false) => { const s = $('edstatus'); s.textContent = t; s.classList.toggle('bad', bad); };
17
+ const armQ = () => `arm=${encodeURIComponent(E.arm)}`;
18
+
19
+ function renderFiles() {
20
+ $('edfiles').replaceChildren(...E.files.map((f) => h('li', {}, h('button', { type: 'button', class: f.path === E.cur ? 'active' : '', 'data-file': f.path },
21
+ h('span', { class: 'fname', text: f.path }), dirty(f.path) ? h('span', { class: 'dotmark', text: '●', title: 'Unsaved changes' }) : null))));
22
+ }
23
+ function renderTabs() {
24
+ $('edtabs').replaceChildren(...E.tabs.map((p) => h('div', { class: 'edtab' + (p === E.cur ? ' active' : '') },
25
+ h('button', { type: 'button', class: 'edtabname', 'data-tab': p, text: p.split('/').pop() + (dirty(p) ? ' ●' : ''), title: p }),
26
+ h('button', { type: 'button', class: 'edtabx', 'data-close': p, 'aria-label': 'Close ' + p, text: '×' }))));
27
+ }
28
+ function gutter() {
29
+ const code = $('edcode'), lines = code.value.split('\n').length;
30
+ $('edgutter').textContent = Array.from({ length: lines }, (_, i) => i + 1).join('\n');
31
+ $('edgutter').scrollTop = code.scrollTop;
32
+ }
33
+ function caret() {
34
+ const code = $('edcode'), before = code.value.slice(0, code.selectionStart).split('\n');
35
+ $('edpos').textContent = `Ln ${before.length}, Col ${before[before.length - 1].length + 1}`;
36
+ }
37
+ function show() {
38
+ const p = E.cur, b = E.buf[p];
39
+ $('edcode').hidden = !p; $('edgutter').hidden = !p; $('edempty').hidden = !!p;
40
+ if (!p) { $('edpath').textContent = ''; $('edsave').disabled = true; renderFiles(); renderTabs(); return; }
41
+ $('edcode').value = b.text;
42
+ $('edcode').readOnly = !!b.readonly || E.busy;
43
+ $('edpath').textContent = p + (b.readonly ? ' (too large to edit here)' : '');
44
+ $('edsave').disabled = !dirty(p) || !!b.readonly;
45
+ gutter(); caret(); renderFiles(); renderTabs();
46
+ status(dirty(p) ? 'Unsaved changes' : 'Saved');
47
+ }
48
+ async function openFile(p) {
49
+ if (!E.buf[p]) {
50
+ const d = await api(`/api/studio/run/${E.run}/file?${armQ()}&path=${encodeURIComponent(p)}&full=1`);
51
+ E.buf[p] = { text: d.content, orig: d.content, readonly: d.truncated };
52
+ }
53
+ if (!E.tabs.includes(p)) E.tabs.push(p);
54
+ E.cur = p;
55
+ show();
56
+ }
57
+ function closeTab(p) {
58
+ if (dirty(p) && !window.confirm(`Close ${p} and lose the unsaved changes?`)) return;
59
+ const i = E.tabs.indexOf(p);
60
+ E.tabs.splice(i, 1);
61
+ delete E.buf[p];
62
+ if (E.cur === p) E.cur = E.tabs[Math.min(i, E.tabs.length - 1)] || '';
63
+ if (E.cur) openFile(E.cur); else show();
64
+ }
65
+ async function refreshFiles() {
66
+ const d = await api(`/api/studio/run/${E.run}/files?${armQ()}`);
67
+ E.files = d.files;
68
+ renderFiles();
69
+ }
70
+ async function save(p = E.cur) {
71
+ const b = E.buf[p];
72
+ if (!b || !dirty(p) || b.readonly) return true;
73
+ try {
74
+ const d = await api(`/api/studio/run/${E.run}/file`, { method: 'PUT', body: JSON.stringify({ arm: E.arm, path: p, content: b.text }) });
75
+ b.orig = b.text;
76
+ await refreshFiles();
77
+ status(d.saved_to_disk ? 'Saved' : 'Saved (this build was a dry run, so it lives in this session only)');
78
+ if (p === E.cur) { $('edsave').disabled = true; renderTabs(); }
79
+ return true;
80
+ } catch (e) { status('Could not save: ' + e.message, true); return false; }
81
+ }
82
+ async function saveAll() { for (const p of Object.keys(E.buf)) if (!(await save(p))) return false; return true; }
83
+
84
+ // ---------------------------------------------------------------- Ask Pal
85
+ function meter(session) {
86
+ const m = $('edmeter');
87
+ if (!session || !session.asks) { m.textContent = 'Each request goes through the token-saving layer.'; return; }
88
+ m.replaceChildren(h('b', { text: n(session.pal_tokens) }), ` tokens used over ${session.asks} request${session.asks > 1 ? 's' : ''}. A plain chat that resends the project would use about `,
89
+ h('b', { text: n(session.plain_estimate) }), session.saved_pct > 0 ? ` (${session.saved_pct}% saved, estimated).` : '. Pal has not saved tokens yet: it pays off as the project and the conversation grow.');
90
+ }
91
+ function renderProposal(pr) {
92
+ E.proposal = pr;
93
+ const box = $('eddiff');
94
+ box.hidden = !pr;
95
+ $('edcode').hidden = !!pr || !E.cur; $('edgutter').hidden = !!pr || !E.cur; $('edempty').hidden = !!pr || !!E.cur;
96
+ if (!pr) { box.replaceChildren(); return; }
97
+ const parts = pr.files.map((f) => h('section', { class: 'dfile' },
98
+ h('div', { class: 'dhead' }, h('b', { text: f.path }), f.new ? ' (new file)' : ''),
99
+ h('pre', { class: 'dlines' }, ...f.diff.map(([t, x]) => h('span', { class: 'dl d-' + t, text: (t === 'add' ? '+ ' : t === 'del' ? '- ' : t === 'hunk' ? '' : ' ') + x + '\n' })))));
100
+ box.replaceChildren(h('div', { class: 'dbar' }, h('span', { text: `Pal suggests changing ${pr.files.length} file${pr.files.length > 1 ? 's' : ''}` }),
101
+ h('span', { class: 'sp' }), h('button', { type: 'button', class: 'btn', id: 'edreject', text: 'Reject' }), h('button', { type: 'button', class: 'btn primary', id: 'edaccept', text: 'Accept' })), ...parts);
102
+ }
103
+ async function accept() {
104
+ const pr = E.proposal;
105
+ if (!pr) return;
106
+ try {
107
+ for (const f of pr.files) {
108
+ await api(`/api/studio/run/${E.run}/file`, { method: 'PUT', body: JSON.stringify({ arm: E.arm, path: f.path, content: f.content }) });
109
+ if (E.buf[f.path]) E.buf[f.path] = { text: f.content, orig: f.content, readonly: false };
110
+ }
111
+ await refreshFiles();
112
+ say(`Applied to ${pr.files.map((f) => f.path).join(', ')}.`, 'ok');
113
+ renderProposal(null);
114
+ const first = pr.files[0].path;
115
+ if (!E.tabs.includes(first)) await openFile(first); else show();
116
+ } catch (e) { say('Could not apply the change: ' + e.message, 'bad'); }
117
+ }
118
+ function reject() { renderProposal(null); say('Rejected. Nothing changed.'); show(); }
119
+ function busy(b) { E.busy = b; $('edsend').disabled = b || !$('edask').value.trim(); $('edask').disabled = b; $('edcode').readOnly = b || !!(E.cur && E.buf[E.cur].readonly); $('edstop').hidden = !b; }
120
+ async function ask(text) {
121
+ if (E.busy || !text) return;
122
+ if (E.proposal) renderProposal(null);
123
+ say(text, 'me');
124
+ busy(true);
125
+ if (!(await saveAll())) { say('Fix the save problem first, then ask again.', 'bad'); busy(false); return; }
126
+ const pending = say('Pal is reading the project...', 'wait');
127
+ const mine = ++E.seq;
128
+ E.ctl = new AbortController();
129
+ try {
130
+ const d = await post(`/api/studio/run/${E.run}/ask`, { arm: E.arm, path: E.cur, instruction: text, api_key: studio.state.apiKey || undefined }, E.ctl.signal);
131
+ if (mine !== E.seq) return;
132
+ pending.remove();
133
+ if (d.message) say(d.message);
134
+ d.refused.forEach((r) => say(`Refused ${r.path}: ${r.reason}`, 'bad'));
135
+ if (d.note) say(d.note);
136
+ const u = d.usage;
137
+ say(`Read ${n(u.prompt_tokens)} tokens, wrote ${n(u.completion_tokens)}${u.helper_tokens ? `, helper ${n(u.helper_tokens)}` : ''} in ${u.seconds}s. A plain chat would have read about ${n(u.plain_estimate - u.completion_tokens)}.`, 'meta');
138
+ meter(d.session);
139
+ if (d.files.length) renderProposal(d); else if (!d.message) say('Pal did not suggest any change.');
140
+ } catch (e) {
141
+ if (mine !== E.seq) return;
142
+ pending.remove();
143
+ say(e.name === 'AbortError' ? 'Stopped.' : 'Something went wrong: ' + e.message, 'bad');
144
+ } finally { if (mine === E.seq) busy(false); }
145
+ }
146
+
147
+ // ---------------------------------------------------------------- opening and closing the editor
148
+ function pickArm(R) {
149
+ const arms = Object.entries((R.summary || {}).arms || {}).filter(([, a]) => a.files > 0).map(([k]) => k);
150
+ return arms.includes('pal') ? 'pal' : arms[arms.length - 1] || '';
151
+ }
152
+ studio.editorArm = pickArm;
153
+ async function open(R) {
154
+ const arm = pickArm(R);
155
+ if (!arm) return;
156
+ if (E.run !== R.id || E.arm !== arm) {
157
+ Object.assign(E, { run: R.id, arm, files: [], tabs: [], cur: '', buf: {}, proposal: null });
158
+ $('edmsgs').replaceChildren();
159
+ say('Ask me to change the open file, add a feature, or explain the code. I will show you the change first, and nothing is saved until you accept it.');
160
+ meter(null);
161
+ renderProposal(null);
162
+ $('eddownload').href = `/api/studio/run/${R.id}/download?arm=${encodeURIComponent(arm)}`;
163
+ }
164
+ document.querySelector('.mainwrap').hidden = true;
165
+ $('editor').hidden = false;
166
+ $('viewchat').classList.remove('active'); $('vieweditor').classList.add('active');
167
+ $('side').classList.remove('open');
168
+ await refreshFiles();
169
+ const first = E.cur || (['index.html', 'main.py', 'readme.md'].map((w) => E.files.find((f) => f.path.toLowerCase() === w)).find(Boolean) || E.files[0] || {}).path || '';
170
+ if (first) await openFile(first); else show();
171
+ }
172
+ function close() {
173
+ $('editor').hidden = true;
174
+ document.querySelector('.mainwrap').hidden = false;
175
+ $('vieweditor').classList.remove('active'); $('viewchat').classList.add('active');
176
+ }
177
+ studio.openEditor = open;
178
+
179
+ $('viewchat').addEventListener('click', close);
180
+ $('vieweditor').addEventListener('click', () => { const R = studio.state.runs.slice().reverse().find((r) => pickArm(r)); if (R) open(R); });
181
+ $('edfiles').addEventListener('click', (e) => { const b = e.target.closest('button[data-file]'); if (b && !E.busy) openFile(b.dataset.file); });
182
+ $('edtabs').addEventListener('click', (e) => {
183
+ const c = e.target.closest('button[data-close]'), t = e.target.closest('button[data-tab]');
184
+ if (c) closeTab(c.dataset.close); else if (t) openFile(t.dataset.tab);
185
+ });
186
+ $('edcode').addEventListener('input', () => { const b = E.buf[E.cur]; if (!b) return; b.text = $('edcode').value; $('edsave').disabled = !dirty(E.cur); status(dirty(E.cur) ? 'Unsaved changes' : 'Saved'); gutter(); renderTabs(); renderFiles(); });
187
+ $('edcode').addEventListener('scroll', () => { $('edgutter').scrollTop = $('edcode').scrollTop; });
188
+ ['keyup', 'click'].forEach((ev) => $('edcode').addEventListener(ev, caret));
189
+ $('edcode').addEventListener('keydown', (e) => {
190
+ if (e.key === 'Tab' && !e.shiftKey) { // a code editor keeps the focus and inserts two spaces
191
+ e.preventDefault();
192
+ const c = $('edcode'), a = c.selectionStart;
193
+ c.setRangeText(' ', a, c.selectionEnd, 'end');
194
+ c.dispatchEvent(new Event('input'));
195
+ }
196
+ });
197
+ $('editor').addEventListener('keydown', (e) => { if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') { e.preventDefault(); save(); } });
198
+ $('edsave').addEventListener('click', () => save());
199
+ $('eddiff').addEventListener('click', (e) => { if (e.target.closest('#edaccept')) accept(); else if (e.target.closest('#edreject')) reject(); });
200
+ $('edform').addEventListener('submit', (e) => { e.preventDefault(); const t = $('edask').value.trim(); $('edask').value = ''; ask(t); });
201
+ $('edask').addEventListener('input', () => { $('edsend').disabled = E.busy || !$('edask').value.trim(); });
202
+ $('edask').addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); $('edform').requestSubmit(); } });
203
+ $('edstop').addEventListener('click', () => { if (E.ctl) E.ctl.abort(); });
204
+ $('ednew').addEventListener('submit', async (e) => {
205
+ e.preventDefault();
206
+ const name = $('ednewname').value.trim();
207
+ if (!name || !has()) return;
208
+ try {
209
+ await api(`/api/studio/run/${E.run}/file`, { method: 'PUT', body: JSON.stringify({ arm: E.arm, path: name, content: '' }) });
210
+ $('ednewname').value = '';
211
+ await refreshFiles();
212
+ const f = E.files.find((x) => x.path.toLowerCase() === name.toLowerCase().replace(/\\/g, '/'));
213
+ await openFile(f ? f.path : name);
214
+ } catch (err) { status('Could not create the file: ' + err.message, true); }
215
+ });
216
+ window.addEventListener('beforeunload', (e) => { if (Object.keys(E.buf).some(dirty)) { e.preventDefault(); e.returnValue = ''; } });
217
+ studio.editor = E;
218
+ });
219
+ })();
@@ -0,0 +1,3 @@
1
+ Inter, Copyright 2016-2023 The Inter Project Authors (https://github.com/rsms/inter).
2
+ Licensed under the SIL Open Font License, Version 1.1 (https://openfontlicense.org).
3
+ These files are a Latin subset of the unmodified design, converted to WOFF2.
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,5 @@
1
+ /* Inter (https://rsms.me/inter), SIL Open Font License 1.1: Latin subset, served from this app so no page loads anything from another site. */
2
+ @font-face{font-family:"Inter";font-style:normal;font-weight:400;font-display:swap;src:url("/static/fonts/inter-400.woff2") format("woff2")}
3
+ @font-face{font-family:"Inter";font-style:normal;font-weight:500;font-display:swap;src:url("/static/fonts/inter-500.woff2") format("woff2")}
4
+ @font-face{font-family:"Inter";font-style:normal;font-weight:600;font-display:swap;src:url("/static/fonts/inter-600.woff2") format("woff2")}
5
+ @font-face{font-family:"Inter";font-style:normal;font-weight:700;font-display:swap;src:url("/static/fonts/inter-700.woff2") format("woff2")}
Binary file
Binary file
Binary file