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/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)
|
pal/web/__init__.py
ADDED