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/tokens.py ADDED
@@ -0,0 +1,21 @@
1
+ """Token counting. The default is a local estimate: it costs nothing and never touches the
2
+ network. `provider_counter` asks the model's own tokenizer endpoint, which is exact but adds a
3
+ network round trip per call, so it is opt-in (settings.token_counter = "provider")."""
4
+ from __future__ import annotations
5
+
6
+ from collections.abc import Callable
7
+
8
+
9
+ def estimate_tokens(text: str) -> int:
10
+ """~4 characters per token, at least 1 for non-empty text."""
11
+ return max(1, len(text) // 4) if text else 0
12
+
13
+
14
+ def estimate_messages(messages: list[dict]) -> int:
15
+ return sum(estimate_tokens(str(m.get("content", ""))) + 4 for m in messages)
16
+
17
+
18
+ def make_counter(mode: str, core=None) -> Callable[[str], int]:
19
+ if mode == "provider" and core is not None:
20
+ return core.token_count
21
+ return estimate_tokens
pal/tool_catalog.py ADDED
@@ -0,0 +1,107 @@
1
+ """Every tool in Pal, by its human name: the single list the docs and `pal tools` are built from.
2
+
3
+ A test compares this catalogue with what is really registered (the chat's tool box, the SDK, the optional OS tools), so it cannot
4
+ drift: a tool that is added without an entry here, or listed here without existing, fails the tests.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from collections.abc import Mapping
9
+ from dataclasses import dataclass
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class ToolEntry:
14
+ name: str # what the model and the Tool Manager call it
15
+ title: str # its human name, like "File Read Tool"
16
+ group: str
17
+ summary: str
18
+ where: str # who can use it, and what switches it on
19
+
20
+
21
+ FILES = "Files and directories"
22
+ DOCS = "Your documents (PDF, Word, text ...)"
23
+ CODE = "Your code project (graphify)"
24
+ CALC = "Calculation and text"
25
+ LOOKUP = "Search and lookup"
26
+ MEDIA = "Images and audio"
27
+ SYSTEM = "System"
28
+
29
+ _SVC = "chat, off until named in tools.services"
30
+ _NET = "chat, off until tools.network is on"
31
+ _DOC = "chat, offered when a document is attached"
32
+ _PRJ = "chat, offered when a project is loaded"
33
+ _OS = "SDK agents, opt-in (register_os_tools)"
34
+
35
+ CATALOG: tuple[ToolEntry, ...] = (
36
+ ToolEntry("read_file", "File Read Tool", FILES, "Read a text file in the agent's workspace.", _OS),
37
+ ToolEntry("write_file", "File Writer Tool", FILES, "Write text to a workspace file (an overwrite asks first).", _OS),
38
+ ToolEntry("list_files", "Directory Read Tool", FILES, "List the files in the workspace.", _OS),
39
+ ToolEntry("save_file", "File Save Tool", FILES, "Save a new file to the workspace for download.", "chat, agents that list it"),
40
+ ToolEntry("file_to_markdown", "File to Markdown Converter Tool", FILES,
41
+ "Convert a PDF, Word, PowerPoint, Excel, EPUB, HTML or LaTeX file to markdown and save it.",
42
+ "chat, when a convertible file is in the workspace; SDK agents (opt-in)"),
43
+ ToolEntry("search_documents", "Document Search Tool", DOCS, "Find the parts of your attached documents that match a query.", _DOC),
44
+ ToolEntry("read_document", "Document Reader Tool", DOCS, "Read one part of an attached document.", _DOC),
45
+ ToolEntry("document_outline", "Document Outline Tool", DOCS, "List a document's parts with headings and sizes.", _DOC),
46
+ ToolEntry("build_code_graph", "Code Graph Builder Tool", CODE, "Index a source zip, or a folder of code, as a graph (graphify).",
47
+ "chat, when a zip or code is in the workspace"),
48
+ ToolEntry("query_code_graph", "Code Graph Query Tool", CODE, "Ask the code graph: matching nodes and source excerpts.", _PRJ),
49
+ ToolEntry("read_source", "Source Reader Tool", CODE, "Read lines of a source file in the project.", _PRJ),
50
+ ToolEntry("find_usages", "Usage Finder Tool", CODE, "Where a function or class is defined and used.", _PRJ),
51
+ ToolEntry("search_source", "Source Search Tool", CODE, "Find lines containing some text.", _PRJ),
52
+ ToolEntry("list_project_files", "Project File List Tool", CODE, "List the project's source files.", _PRJ),
53
+ ToolEntry("calculator", "Calculator Tool", CALC, "Exact arithmetic.", "chat, SDK"),
54
+ ToolEntry("current_time", "Clock Tool", CALC, "The current date and time.", "chat, SDK"),
55
+ ToolEntry("word_count", "Word Count Tool", CALC, "Count the words in a text.", "SDK"),
56
+ ToolEntry("count_tokens", "Token Counter Tool", CALC, "Estimate the tokens in a text (characters divided by 4).", "chat"),
57
+ ToolEntry("estimate_cost", "Cost Estimator Tool", CALC, "Dollars for some tokens at the model's price (LiteLLM's table).", "chat"),
58
+ ToolEntry("summarize_text", "Text Summarizer Tool", CALC, "A short summary of a text, by the local model.", "chat"),
59
+ ToolEntry("wolfram_alpha", "Wolfram|Alpha Tool", CALC, "Computed answers: maths, units, science, facts.", _SVC),
60
+ ToolEntry("currency_convert", "Currency Converter Tool", CALC, "Convert money at central bank reference rates.", _SVC),
61
+ ToolEntry("moon_phase", "Moon Phase Tool", CALC, "The moon's phase for a date, computed locally.", _SVC),
62
+ ToolEntry("words_api", "Words API Tool", CALC, "Definitions, synonyms, antonyms, rhymes, examples.", _SVC),
63
+ ToolEntry("google_search", "Google Search Tool", LOOKUP, "Google results for a query (through SerpApi).", _SVC),
64
+ ToolEntry("wikipedia", "Wikipedia Tool", LOOKUP, "The summary of a Wikipedia article.", _NET),
65
+ ToolEntry("arxiv_search", "arXiv Search Tool", LOOKUP, "Research papers on arXiv.", _NET),
66
+ ToolEntry("http_get", "HTTP GET Tool", LOOKUP, "Fetch a URL through the guarded network interface.", _OS),
67
+ ToolEntry("image_caption", "Image Caption Tool", MEDIA, "Describe an uploaded image.", _SVC + ", needs an uploaded image"),
68
+ ToolEntry("visual_question_answering", "Visual Question Answering Tool", MEDIA, "Answer a question about an uploaded image.",
69
+ _SVC + ", needs an uploaded image"),
70
+ ToolEntry("text_to_image", "Text-to-Image Tool", MEDIA, "Make an image from a description.", _SVC),
71
+ ToolEntry("text_to_audio", "Text-to-Audio Tool", MEDIA, "Turn text into spoken audio.", _SVC),
72
+ ToolEntry("speech_to_text", "Speech-to-Text Tool", MEDIA, "Transcribe an uploaded audio file.", _SVC + ", needs an uploaded audio file"),
73
+ ToolEntry("system_info", "System Info Tool", SYSTEM, "Operating system, memory and load.", _OS),
74
+ )
75
+
76
+ GROUPS: tuple[str, ...] = (FILES, DOCS, CODE, CALC, LOOKUP, MEDIA, SYSTEM)
77
+ BY_NAME: dict[str, ToolEntry] = {e.name: e for e in CATALOG}
78
+
79
+ # which environment variable each service tool needs (the services' own table is the source of truth)
80
+ def key_needed(name: str) -> str | None:
81
+ from pal.chat.services import BY_NAME as SERVICES
82
+ spec = SERVICES.get(name)
83
+ return spec.key_env if spec else None
84
+
85
+
86
+ def state(entry: ToolEntry, tools_cfg, env: Mapping[str, str]) -> str:
87
+ """For a tool that has to be switched on: whether it is on and ready, from the settings and the environment."""
88
+ from pal.chat.services import BY_NAME as SERVICES
89
+ if entry.name in SERVICES:
90
+ if "all" not in tools_cfg.services and entry.name not in tools_cfg.services:
91
+ return "off"
92
+ need = key_needed(entry.name)
93
+ return f"needs {need}" if need and not env.get(need) else "ready"
94
+ if entry.name in ("wikipedia", "arxiv_search"):
95
+ return "ready" if tools_cfg.network else "off"
96
+ return ""
97
+
98
+
99
+ def render(tools_cfg, env: Mapping[str, str]) -> str:
100
+ lines = [f"Pal tools ({len(CATALOG)})", ""]
101
+ for group in GROUPS:
102
+ lines.append(group)
103
+ for e in (x for x in CATALOG if x.group == group):
104
+ st = state(e, tools_cfg, env)
105
+ lines.append(f" {e.title:<36} {e.name:<26} {e.summary} [{e.where}{'; ' + st if st else ''}]")
106
+ lines.append("")
107
+ return "\n".join(lines).rstrip() + "\n"
@@ -0,0 +1,17 @@
1
+ from pal.tool_manager.components.base import ToolCallRequest, ToolCallResult, ToolParam, ToolSpec
2
+ from pal.tool_manager.components.conflict_resolver import ToolConflictResolver
3
+ from pal.tool_manager.components.param_validator import ToolValidationError
4
+ from pal.tool_manager.components.tool_registry import ToolDependencyError, ToolRegistry
5
+ from pal.tool_manager.pipeline import ToolManagerPipeline
6
+
7
+ __all__ = [
8
+ "ToolManagerPipeline",
9
+ "ToolCallRequest",
10
+ "ToolCallResult",
11
+ "ToolParam",
12
+ "ToolSpec",
13
+ "ToolConflictResolver",
14
+ "ToolValidationError",
15
+ "ToolDependencyError",
16
+ "ToolRegistry",
17
+ ]
@@ -0,0 +1,82 @@
1
+ """Built-in read-only tools. `safe_eval` is an AST-based calculator: it never calls eval(), only
2
+ allows numbers and + - * / // % ** with parentheses, and refuses expressions that would run for
3
+ a long time or produce enormous numbers."""
4
+ from __future__ import annotations
5
+
6
+ import ast
7
+ import operator
8
+ import time
9
+ from collections.abc import Callable
10
+ from typing import Union
11
+
12
+ from pal.tool_manager.components.base import ToolParam
13
+ from pal.tool_manager.pipeline import ToolManagerPipeline
14
+
15
+ Number = Union[int, float]
16
+
17
+ _BIN: dict[type, Callable[[Number, Number], Number]] = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv,
18
+ ast.FloorDiv: operator.floordiv, ast.Mod: operator.mod, ast.Pow: operator.pow}
19
+ _UNARY: dict[type, Callable[[Number], Number]] = {ast.UAdd: operator.pos, ast.USub: operator.neg}
20
+ _MAX_LEN = 200
21
+ _MAX_BITS = 20_000
22
+
23
+
24
+ class CalculatorError(ValueError):
25
+ pass
26
+
27
+
28
+ def _pow(a: Number, b: Number) -> Number:
29
+ if isinstance(a, int) and isinstance(b, int) and b > 0 and a not in (0, 1, -1) and a.bit_length() * b > _MAX_BITS:
30
+ raise CalculatorError("number too large")
31
+ if isinstance(b, float) and abs(b) > 1e6:
32
+ raise CalculatorError("exponent too large")
33
+ try:
34
+ r = a ** b
35
+ except (OverflowError, ZeroDivisionError) as e:
36
+ raise CalculatorError(str(e)) from e
37
+ if isinstance(r, complex):
38
+ raise CalculatorError("result is not a real number")
39
+ return r
40
+
41
+
42
+ def _ev(node: ast.AST) -> Number:
43
+ if isinstance(node, ast.Expression):
44
+ return _ev(node.body)
45
+ if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)) and not isinstance(node.value, bool):
46
+ return node.value
47
+ if isinstance(node, ast.UnaryOp) and type(node.op) in _UNARY:
48
+ return _UNARY[type(node.op)](_ev(node.operand))
49
+ if isinstance(node, ast.BinOp) and type(node.op) in _BIN:
50
+ a, b = _ev(node.left), _ev(node.right)
51
+ if isinstance(node.op, ast.Pow):
52
+ return _pow(a, b)
53
+ try:
54
+ return _BIN[type(node.op)](a, b)
55
+ except ZeroDivisionError as e:
56
+ raise CalculatorError("division by zero") from e
57
+ raise CalculatorError("only numbers and + - * / // % ** ( ) are allowed")
58
+
59
+
60
+ def safe_eval(expression: str) -> Number:
61
+ text = expression.strip().replace("^", "**").replace("\u00d7", "*").replace("\u00f7", "/")
62
+ if not text or len(text) > _MAX_LEN:
63
+ raise CalculatorError("empty or too long")
64
+ try:
65
+ tree = ast.parse(text, mode="eval")
66
+ except (SyntaxError, ValueError, RecursionError) as e:
67
+ raise CalculatorError("not a valid arithmetic expression") from e
68
+ try:
69
+ result = _ev(tree)
70
+ except RecursionError as e:
71
+ raise CalculatorError("expression too deeply nested") from e
72
+ if isinstance(result, float):
73
+ if result != result or result in (float("inf"), float("-inf")):
74
+ raise CalculatorError("result is not finite")
75
+ return int(result) if result.is_integer() and abs(result) < 1e15 else float(f"{result:.12g}")
76
+ return result
77
+
78
+
79
+ def register_builtin_tools(tm: ToolManagerPipeline) -> None:
80
+ """calculator and current_time: no state, no files, no network."""
81
+ tm.register_tool("calculator", lambda: safe_eval, params=[ToolParam("expression", str)])
82
+ tm.register_tool("current_time", lambda: (lambda: time.strftime("%Y-%m-%d %H:%M:%S %z")), params=[])
@@ -0,0 +1,19 @@
1
+ from pal.tool_manager.components.base import ToolCallRequest, ToolCallResult, ToolParam, ToolSpec
2
+ from pal.tool_manager.components.conflict_resolver import ToolConflictResolver
3
+ from pal.tool_manager.components.param_validator import ToolValidationError, validate_params
4
+ from pal.tool_manager.components.tool_call_queue import QueuedToolCall, ToolCallQueue
5
+ from pal.tool_manager.components.tool_registry import ToolDependencyError, ToolRegistry
6
+
7
+ __all__ = [
8
+ "ToolCallRequest",
9
+ "ToolCallResult",
10
+ "ToolParam",
11
+ "ToolSpec",
12
+ "ToolConflictResolver",
13
+ "ToolValidationError",
14
+ "validate_params",
15
+ "QueuedToolCall",
16
+ "ToolCallQueue",
17
+ "ToolDependencyError",
18
+ "ToolRegistry",
19
+ ]
@@ -0,0 +1,53 @@
1
+ """
2
+ Standardized tool interface and call request/response shapes (AIOS
3
+ section 3.7). Every tool the tool manager loads conforms to ToolSpec --
4
+ callers never invoke a tool's underlying callable directly, only ever
5
+ through ToolManagerPipeline.invoke(), which validates parameters and
6
+ resolves parallel-access conflicts uniformly regardless of what the
7
+ tool itself actually does.
8
+ """
9
+ import time
10
+ import uuid
11
+ from dataclasses import dataclass, field
12
+ from typing import Any, Callable, Optional
13
+
14
+
15
+ @dataclass
16
+ class ToolParam:
17
+ name: str
18
+ type: type
19
+ required: bool = True
20
+ default: Any = None
21
+
22
+
23
+ @dataclass
24
+ class ToolSpec:
25
+ """A tool's standardized description. `loader` is called at most
26
+ once, the first time the tool is invoked by name -- "dynamically
27
+ loads the tool instance including executables initialization" means
28
+ exactly this: constructing/initializing whatever the tool actually
29
+ needs (a client, a subprocess handle, a loaded model, ...) is
30
+ deferred until the tool is first used, not paid for every tool ever
31
+ registered."""
32
+
33
+ name: str
34
+ loader: Callable[[], Callable[..., Any]] # returns the callable that actually runs the tool
35
+ params: list[ToolParam] = field(default_factory=list)
36
+ parallel_limit: Optional[int] = None # None = unlimited concurrent calls
37
+ dependencies: list[str] = field(default_factory=list) # importable module names required at load time
38
+
39
+
40
+ @dataclass
41
+ class ToolCallRequest:
42
+ agent_id: str
43
+ tool_name: str
44
+ args: dict
45
+ id: str = field(default_factory=lambda: str(uuid.uuid4()))
46
+ submitted_at: float = field(default_factory=time.time)
47
+
48
+
49
+ @dataclass
50
+ class ToolCallResult:
51
+ ok: bool
52
+ result: Any = None
53
+ error: Optional[str] = None
@@ -0,0 +1,27 @@
1
+ """
2
+ Resolution of tool call conflicts (AIOS section 3.7): tools with a
3
+ parallel-access limit (ToolSpec.parallel_limit) are tracked in a
4
+ hashmap of tool_name -> current in-flight call count. A request is only
5
+ admitted if doing so would not push that count past the tool's limit.
6
+ """
7
+ from typing import Optional
8
+
9
+
10
+ class ToolConflictResolver:
11
+ def __init__(self):
12
+ self._in_flight: dict[str, int] = {}
13
+
14
+ def can_acquire(self, tool_name: str, parallel_limit: Optional[int]) -> bool:
15
+ if parallel_limit is None:
16
+ return True
17
+ return self._in_flight.get(tool_name, 0) < parallel_limit
18
+
19
+ def acquire(self, tool_name: str) -> None:
20
+ self._in_flight[tool_name] = self._in_flight.get(tool_name, 0) + 1
21
+
22
+ def release(self, tool_name: str) -> None:
23
+ if tool_name in self._in_flight:
24
+ self._in_flight[tool_name] = max(0, self._in_flight[tool_name] - 1)
25
+
26
+ def in_flight_count(self, tool_name: str) -> int:
27
+ return self._in_flight.get(tool_name, 0)
@@ -0,0 +1,41 @@
1
+ """
2
+ Parameter validation before execution -- "incorporating parameter
3
+ validation before execution to prevent tool crashes." Runs before a
4
+ tool's underlying callable is ever invoked, so a malformed call fails
5
+ with a clear ToolValidationError instead of an arbitrary exception (or
6
+ worse, silently wrong behavior) inside the tool itself.
7
+ """
8
+ from pal.tool_manager.components.base import ToolParam
9
+
10
+
11
+ class ToolValidationError(ValueError):
12
+ pass
13
+
14
+
15
+ def validate_params(tool_name: str, params: list[ToolParam], args: dict) -> dict:
16
+ """Returns a normalized args dict (missing optional params filled in
17
+ with their declared defaults) or raises ToolValidationError."""
18
+ validated = {}
19
+ declared_names = {p.name for p in params}
20
+
21
+ for p in params:
22
+ if p.name in args:
23
+ value = args[p.name]
24
+ if p.type is float and isinstance(value, int) and not isinstance(value, bool):
25
+ value = float(value) # JSON writes 100 for 100.0
26
+ if not isinstance(value, p.type) or (p.type in (int, float) and isinstance(value, bool)):
27
+ raise ToolValidationError(
28
+ f"{tool_name}: parameter '{p.name}' expected {p.type.__name__}, "
29
+ f"got {type(value).__name__}"
30
+ )
31
+ validated[p.name] = value
32
+ elif p.required:
33
+ raise ToolValidationError(f"{tool_name}: missing required parameter '{p.name}'")
34
+ else:
35
+ validated[p.name] = p.default
36
+
37
+ unknown = set(args) - declared_names
38
+ if unknown:
39
+ raise ToolValidationError(f"{tool_name}: unexpected parameter(s): {sorted(unknown)}")
40
+
41
+ return validated
@@ -0,0 +1,111 @@
1
+ """
2
+ Queued tool-call dispatch with conflict skip-ahead (AIOS section 3.7):
3
+ "Request processing involves hashmap verification against usage and
4
+ parallel limits; upon detecting conflicts, the system advances to
5
+ subsequent queue requests until identifying a conflict-free candidate."
6
+
7
+ Unlike the LLM syscall scheduler's FIFO/Round-Robin (section 3.3, which
8
+ always processes requests in strict arrival or time-sliced order), a
9
+ tool-call queue that hits a conflict does NOT block everything behind
10
+ it: dispatch_next() scans forward past conflicting requests to find the
11
+ first one that's runnable right now and executes that, leaving the
12
+ conflicting ones in place (not requeued at the back -- their position
13
+ in line doesn't change, only which one runs next does) for a later
14
+ dispatch_next() call once capacity frees up.
15
+ """
16
+ import time
17
+ import uuid
18
+ from collections import deque
19
+ from dataclasses import dataclass, field
20
+ from typing import Optional
21
+
22
+ from pal.tool_manager.components.base import ToolCallRequest, ToolCallResult
23
+ from pal.tool_manager.components.conflict_resolver import ToolConflictResolver
24
+ from pal.tool_manager.components.param_validator import validate_params
25
+ from pal.tool_manager.components.tool_registry import ToolRegistry
26
+
27
+
28
+ @dataclass
29
+ class QueuedToolCall:
30
+ request: ToolCallRequest
31
+ id: str = field(default_factory=lambda: str(uuid.uuid4()))
32
+ submitted_at: float = field(default_factory=time.time)
33
+ status: str = "queued" # queued | done | error
34
+ result: Optional[ToolCallResult] = None
35
+
36
+
37
+ class ToolCallQueue:
38
+ def __init__(self, registry: ToolRegistry, conflict_resolver: ToolConflictResolver):
39
+ self.registry = registry
40
+ self.conflicts = conflict_resolver
41
+ self.queue: deque[QueuedToolCall] = deque()
42
+ self.requests: dict[str, QueuedToolCall] = {}
43
+
44
+ def submit(self, agent_id: str, tool_name: str, args: dict) -> str:
45
+ request = ToolCallRequest(agent_id=agent_id, tool_name=tool_name, args=args)
46
+ entry = QueuedToolCall(request=request)
47
+ self.requests[entry.id] = entry
48
+ self.queue.append(entry)
49
+ return entry.id
50
+
51
+ def dispatch_next(self) -> Optional[QueuedToolCall]:
52
+ """Scans the queue from the front for the first request whose
53
+ tool currently has capacity (hashmap check against its
54
+ parallel_limit), executes it, and removes it from the queue.
55
+ Returns None if the queue is empty or every queued request
56
+ currently conflicts."""
57
+ for entry in list(self.queue):
58
+ spec = self.registry.spec(entry.request.tool_name)
59
+ if not self.conflicts.can_acquire(entry.request.tool_name, spec.parallel_limit):
60
+ continue # conflict -- advance to the next queued request
61
+
62
+ self.queue.remove(entry)
63
+ self._execute(entry)
64
+ return entry
65
+
66
+ return None # nothing runnable right now
67
+
68
+ def _execute(self, entry: QueuedToolCall) -> None:
69
+ req = entry.request
70
+ spec = self.registry.spec(req.tool_name)
71
+ self.conflicts.acquire(req.tool_name)
72
+ try:
73
+ validated_args = validate_params(req.tool_name, spec.params, req.args)
74
+ fn = self.registry.get_callable(req.tool_name)
75
+ output = fn(**validated_args)
76
+ entry.result = ToolCallResult(ok=True, result=output)
77
+ entry.status = "done"
78
+ except Exception as e:
79
+ entry.result = ToolCallResult(ok=False, error=str(e))
80
+ entry.status = "error"
81
+ finally:
82
+ self.conflicts.release(req.tool_name)
83
+
84
+ def drain(self, max_iterations: int = 10_000) -> list[QueuedToolCall]:
85
+ """Repeatedly dispatch_next() until the queue is empty or
86
+ nothing left is runnable. Since this project executes tools
87
+ synchronously, a slot acquired in _execute() is released again
88
+ before dispatch_next() returns -- so a conflict only actually
89
+ blocks drain() here if the SAME tool's limit was already
90
+ exhausted by an in-flight call held open from OUTSIDE this
91
+ queue (e.g. another agent process using the same
92
+ ToolConflictResolver). max_iterations guards against that
93
+ making no progress."""
94
+ finished = []
95
+ i = 0
96
+ while self.queue and i < max_iterations:
97
+ entry = self.dispatch_next()
98
+ if entry is None:
99
+ break # every remaining request currently conflicts
100
+ finished.append(entry)
101
+ i += 1
102
+ return finished
103
+
104
+ def get_result(self, request_id: str) -> QueuedToolCall:
105
+ return self.requests[request_id]
106
+
107
+ def forget(self, request_id: str) -> None:
108
+ self.requests.pop(request_id, None)
109
+
110
+ def pending_count(self) -> int:
111
+ return len(self.queue)
@@ -0,0 +1,60 @@
1
+ """
2
+ Standardized tool loading (AIOS section 3.7): tools are registered by
3
+ ToolSpec but not actually constructed until first invoked by name --
4
+ "dynamically loads the tool instance including executables
5
+ initialization and dependencies verification" happens exactly once,
6
+ lazily, here, and the resulting callable is cached for every later
7
+ call.
8
+ """
9
+ import importlib
10
+
11
+ from pal.tool_manager.components.base import ToolSpec
12
+
13
+
14
+ class ToolDependencyError(ImportError):
15
+ pass
16
+
17
+
18
+ class ToolRegistry:
19
+ def __init__(self):
20
+ self._specs: dict[str, ToolSpec] = {}
21
+ self._loaded: dict[str, object] = {}
22
+
23
+ def register(self, spec: ToolSpec) -> None:
24
+ self._specs[spec.name] = spec
25
+
26
+ def is_registered(self, name: str) -> bool:
27
+ return name in self._specs
28
+
29
+ def spec(self, name: str) -> ToolSpec:
30
+ if name not in self._specs:
31
+ raise KeyError(f"no tool registered with name '{name}'")
32
+ return self._specs[name]
33
+
34
+ def get_callable(self, name: str):
35
+ """Dynamically loads (once) and returns the tool's underlying
36
+ callable. Verifies every declared dependency imports cleanly
37
+ BEFORE running the tool's own loader, so a missing dependency
38
+ fails with a clear ToolDependencyError naming exactly which
39
+ module is missing, rather than an opaque ImportError raised
40
+ from somewhere inside the tool's own loader/initialization
41
+ code."""
42
+ if name in self._loaded:
43
+ return self._loaded[name]
44
+
45
+ spec = self.spec(name)
46
+ for dep in spec.dependencies:
47
+ try:
48
+ importlib.import_module(dep)
49
+ except ImportError as e:
50
+ raise ToolDependencyError(f"tool '{name}' requires '{dep}', which is not installed") from e
51
+
52
+ instance = spec.loader()
53
+ self._loaded[name] = instance
54
+ return instance
55
+
56
+ def is_loaded(self, name: str) -> bool:
57
+ return name in self._loaded
58
+
59
+ def list_tools(self) -> list[str]:
60
+ return sorted(self._specs.keys())
@@ -0,0 +1,80 @@
1
+ """
2
+ Tool Manager (AIOS section 3.7): standardized tool loading (uniform
3
+ interface, parameter validation before execution, dynamic
4
+ dependency-checked loading by name) and resolution of tool-call
5
+ conflicts for tools with parallel-access limits (a hashmap of
6
+ real-time in-flight counts, and a queue that skips ahead past
7
+ conflicting requests rather than blocking on them).
8
+ """
9
+ from typing import Optional
10
+
11
+ from pal.tool_manager.components.base import ToolCallResult, ToolParam, ToolSpec
12
+ from pal.tool_manager.components.conflict_resolver import ToolConflictResolver
13
+ from pal.tool_manager.components.tool_call_queue import QueuedToolCall, ToolCallQueue
14
+ from pal.tool_manager.components.tool_registry import ToolRegistry
15
+
16
+
17
+ class ToolManagerPipeline:
18
+ def __init__(self):
19
+ self.registry = ToolRegistry()
20
+ self.conflicts = ToolConflictResolver()
21
+ self.queue = ToolCallQueue(self.registry, self.conflicts)
22
+
23
+ def register_tool(
24
+ self,
25
+ name: str,
26
+ loader,
27
+ params: list[ToolParam] = None,
28
+ parallel_limit: Optional[int] = None,
29
+ dependencies: list[str] = None,
30
+ ) -> None:
31
+ self.registry.register(ToolSpec(
32
+ name=name,
33
+ loader=loader,
34
+ params=params or [],
35
+ parallel_limit=parallel_limit,
36
+ dependencies=dependencies or [],
37
+ ))
38
+
39
+ def list_tools(self) -> list[str]:
40
+ return self.registry.list_tools()
41
+
42
+ def invoke(self, agent_id: str, tool_name: str, args: dict) -> ToolCallResult:
43
+ """Synchronous convenience call: submits to the queue and drains
44
+ dispatch attempts until THIS specific request completes (i.e.
45
+ until it's the conflict-free candidate dispatch_next() picks).
46
+ Looks synchronous to the caller even though it went through the
47
+ same centralized queue every tool call does -- same shape as
48
+ StorageSDK and Pipeline's LLM syscall dispatch."""
49
+ if not self.registry.is_registered(tool_name):
50
+ return ToolCallResult(ok=False, error=f"unknown tool: {tool_name}")
51
+ request_id = self.queue.submit(agent_id, tool_name, args)
52
+ try:
53
+ entry = self.queue.get_result(request_id)
54
+ while entry.status == "queued":
55
+ dispatched = self.queue.dispatch_next()
56
+ if dispatched is None:
57
+ # Every queued call currently conflicts -- nothing this
58
+ # loop can do would make progress right now.
59
+ return ToolCallResult(
60
+ ok=False,
61
+ error=f"tool '{tool_name}' could not be dispatched -- all queued calls currently conflict",
62
+ )
63
+ entry = self.queue.get_result(request_id)
64
+ return entry.result
65
+ finally:
66
+ self.queue.forget(request_id)
67
+
68
+ # -- queue-driven path, for callers that want direct control ---------------
69
+
70
+ def submit(self, agent_id: str, tool_name: str, args: dict) -> str:
71
+ return self.queue.submit(agent_id, tool_name, args)
72
+
73
+ def dispatch_next(self) -> Optional[QueuedToolCall]:
74
+ return self.queue.dispatch_next()
75
+
76
+ def drain(self, max_iterations: int = 10_000) -> list[QueuedToolCall]:
77
+ return self.queue.drain(max_iterations=max_iterations)
78
+
79
+ def get_result(self, request_id: str) -> QueuedToolCall:
80
+ return self.queue.get_result(request_id)
@@ -0,0 +1,3 @@
1
+ """No shared utilities yet for tool_manager -- reserved for future
2
+ helpers (e.g. shared parameter-type coercion, tool discovery from a
3
+ manifest file)."""
pal/web/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from pal.web.app import create_app
2
+
3
+ __all__ = ["create_app"]