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/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Pal: a model-agnostic chat layer that cuts tokens and latency.
|
|
2
|
+
|
|
3
|
+
Importing the package is cheap on purpose. The older `Pipeline` (which pulls in the compression
|
|
4
|
+
stack: nltk, spacy, ...) is loaded only if you ask for it.
|
|
5
|
+
"""
|
|
6
|
+
from importlib import import_module
|
|
7
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _version() -> str:
|
|
11
|
+
for name in ("pal-code", "pal-ai", "pal-aios"): # the current name first, then earlier ones
|
|
12
|
+
try:
|
|
13
|
+
return version(name)
|
|
14
|
+
except PackageNotFoundError:
|
|
15
|
+
continue
|
|
16
|
+
return "0.1.0" # a source checkout that is not installed
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__version__ = _version()
|
|
20
|
+
|
|
21
|
+
__all__ = ["Pipeline", "WrapperConfig", "__version__"]
|
|
22
|
+
|
|
23
|
+
_LAZY = {"Pipeline": "pal.pipeline", "WrapperConfig": "pal.llm_wrapper.components.base"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def __getattr__(name: str):
|
|
27
|
+
if name in _LAZY:
|
|
28
|
+
return getattr(import_module(_LAZY[name]), name)
|
|
29
|
+
raise AttributeError(f"module 'pal' has no attribute {name!r}")
|
pal/__main__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from pal.access_manager.components.access_control import AccessController
|
|
2
|
+
from pal.access_manager.components.base import DESTRUCTIVE_OPS, AccessDecision, AccessRequest, OperationType
|
|
3
|
+
from pal.access_manager.components.intervention import UserInterventionGate
|
|
4
|
+
from pal.access_manager.components.privilege_registry import PrivilegeRegistry
|
|
5
|
+
from pal.access_manager.pipeline import AccessManagerPipeline
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"AccessManagerPipeline",
|
|
9
|
+
"AccessDecision",
|
|
10
|
+
"AccessRequest",
|
|
11
|
+
"DESTRUCTIVE_OPS",
|
|
12
|
+
"OperationType",
|
|
13
|
+
"AccessController",
|
|
14
|
+
"UserInterventionGate",
|
|
15
|
+
"PrivilegeRegistry",
|
|
16
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from pal.access_manager.components.access_control import AccessController
|
|
2
|
+
from pal.access_manager.components.base import DESTRUCTIVE_OPS, AccessDecision, AccessRequest, OperationType
|
|
3
|
+
from pal.access_manager.components.intervention import UserInterventionGate
|
|
4
|
+
from pal.access_manager.components.privilege_registry import PrivilegeRegistry
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"AccessController",
|
|
8
|
+
"AccessDecision",
|
|
9
|
+
"AccessRequest",
|
|
10
|
+
"DESTRUCTIVE_OPS",
|
|
11
|
+
"OperationType",
|
|
12
|
+
"UserInterventionGate",
|
|
13
|
+
"PrivilegeRegistry",
|
|
14
|
+
]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Privilege-based access control (AIOS section 3.8): validates a
|
|
3
|
+
cross-agent access request against the privilege-group hashmap BEFORE
|
|
4
|
+
execution -- an agent may only access another agent's resources if
|
|
5
|
+
both are assigned to the same shared privilege domain. An agent acting
|
|
6
|
+
on its OWN resources is always permitted regardless of privilege group
|
|
7
|
+
(3.8 is specifically about cross-agent data access; a resource owner
|
|
8
|
+
always has access to what it owns).
|
|
9
|
+
"""
|
|
10
|
+
from pal.access_manager.components.base import DESTRUCTIVE_OPS, AccessDecision, AccessRequest
|
|
11
|
+
from pal.access_manager.components.privilege_registry import PrivilegeRegistry
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AccessController:
|
|
15
|
+
def __init__(self, registry: PrivilegeRegistry):
|
|
16
|
+
self.registry = registry
|
|
17
|
+
|
|
18
|
+
def check(self, request: AccessRequest) -> AccessDecision:
|
|
19
|
+
requires_confirmation = request.operation in DESTRUCTIVE_OPS
|
|
20
|
+
|
|
21
|
+
if request.requesting_agent_id == request.target_agent_id:
|
|
22
|
+
return AccessDecision(allowed=True, requires_confirmation=requires_confirmation)
|
|
23
|
+
|
|
24
|
+
if self.registry.same_group(request.requesting_agent_id, request.target_agent_id):
|
|
25
|
+
return AccessDecision(allowed=True, requires_confirmation=requires_confirmation)
|
|
26
|
+
|
|
27
|
+
group_a = self.registry.group_of(request.requesting_agent_id)
|
|
28
|
+
group_b = self.registry.group_of(request.target_agent_id)
|
|
29
|
+
if group_a is None or group_b is None:
|
|
30
|
+
unassigned = [
|
|
31
|
+
aid for aid, g in ((request.requesting_agent_id, group_a), (request.target_agent_id, group_b))
|
|
32
|
+
if g is None
|
|
33
|
+
]
|
|
34
|
+
reason = f"agent(s) not assigned a privilege group: {unassigned}"
|
|
35
|
+
else:
|
|
36
|
+
reason = (
|
|
37
|
+
f"'{request.requesting_agent_id}' (group '{group_a}') is not in the same "
|
|
38
|
+
f"privilege group as '{request.target_agent_id}' (group '{group_b}')"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return AccessDecision(allowed=False, requires_confirmation=requires_confirmation, reason=reason)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Access request/decision shapes for the Access Manager (AIOS section
|
|
3
|
+
3.8). An AccessRequest always names both the requesting agent and the
|
|
4
|
+
agent whose resource is being touched -- when they're the same agent,
|
|
5
|
+
there's nothing cross-agent to regulate, only user-intervention (for
|
|
6
|
+
destructive ops) applies.
|
|
7
|
+
"""
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OperationType(str, Enum):
|
|
14
|
+
READ = "read"
|
|
15
|
+
WRITE = "write"
|
|
16
|
+
DELETE = "delete"
|
|
17
|
+
OVERWRITE = "overwrite"
|
|
18
|
+
PRIVILEGE_MODIFY = "privilege_modify"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# "irreversible operations (deletion, overwrite, privilege modification)"
|
|
22
|
+
DESTRUCTIVE_OPS = {OperationType.DELETE, OperationType.OVERWRITE, OperationType.PRIVILEGE_MODIFY}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class AccessRequest:
|
|
27
|
+
requesting_agent_id: str
|
|
28
|
+
target_agent_id: str # whose resource is being accessed -- equal to
|
|
29
|
+
# requesting_agent_id for an agent acting on
|
|
30
|
+
# its own data
|
|
31
|
+
operation: OperationType
|
|
32
|
+
resource: str = "" # a key, tool name, or other resource descriptor -- for logging/audit only
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class AccessDecision:
|
|
37
|
+
allowed: bool
|
|
38
|
+
requires_confirmation: bool
|
|
39
|
+
reason: Optional[str] = None
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
User intervention interface (AIOS section 3.8): irreversible operations
|
|
3
|
+
(deletion, overwrite, privilege modification) require explicit user
|
|
4
|
+
confirmation before proceeding, even when privilege-based access
|
|
5
|
+
control alone would permit them.
|
|
6
|
+
|
|
7
|
+
The actual UI a real deployment asks the user through (a CLI prompt, a
|
|
8
|
+
web confirmation dialog, ...) is out of scope for this project -- what
|
|
9
|
+
this implements is the gate itself: a pluggable confirmation_handler
|
|
10
|
+
callback the surrounding application registers. The default handler
|
|
11
|
+
denies everything, safely, rather than silently approving destructive
|
|
12
|
+
operations when no real confirmation UI has been wired up.
|
|
13
|
+
"""
|
|
14
|
+
from typing import Callable, Optional
|
|
15
|
+
|
|
16
|
+
from pal.access_manager.components.base import AccessRequest
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _default_deny_handler(request: AccessRequest) -> bool:
|
|
20
|
+
return False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class UserInterventionGate:
|
|
24
|
+
def __init__(self, confirmation_handler: Optional[Callable[[AccessRequest], bool]] = None):
|
|
25
|
+
self.confirmation_handler = confirmation_handler or _default_deny_handler
|
|
26
|
+
|
|
27
|
+
def set_handler(self, handler: Callable[[AccessRequest], bool]) -> None:
|
|
28
|
+
self.confirmation_handler = handler
|
|
29
|
+
|
|
30
|
+
def confirm(self, request: AccessRequest) -> bool:
|
|
31
|
+
return bool(self.confirmation_handler(request))
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Privilege-group hashmap (AIOS section 3.8): agent_id -> privilege
|
|
3
|
+
group, enforced by AccessController before any cross-agent access is
|
|
4
|
+
permitted. Each agent is assigned to exactly one group; agents sharing
|
|
5
|
+
a group are in the same "privilege domain" and may access each other's
|
|
6
|
+
resources.
|
|
7
|
+
"""
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PrivilegeRegistry:
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self._groups: dict[str, str] = {}
|
|
14
|
+
|
|
15
|
+
def assign(self, agent_id: str, group: str) -> None:
|
|
16
|
+
self._groups[agent_id] = group
|
|
17
|
+
|
|
18
|
+
def group_of(self, agent_id: str) -> Optional[str]:
|
|
19
|
+
return self._groups.get(agent_id)
|
|
20
|
+
|
|
21
|
+
def same_group(self, agent_a: str, agent_b: str) -> bool:
|
|
22
|
+
group_a = self.group_of(agent_a)
|
|
23
|
+
group_b = self.group_of(agent_b)
|
|
24
|
+
return group_a is not None and group_a == group_b
|
|
25
|
+
|
|
26
|
+
def revoke(self, agent_id: str) -> None:
|
|
27
|
+
self._groups.pop(agent_id, None)
|
|
28
|
+
|
|
29
|
+
def all_assignments(self) -> dict[str, str]:
|
|
30
|
+
return dict(self._groups)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Access Manager (AIOS section 3.8): privilege-based access control for
|
|
3
|
+
cross-agent resource access (components/privilege_registry.py,
|
|
4
|
+
components/access_control.py), plus a user-intervention gate for
|
|
5
|
+
irreversible operations (components/intervention.py).
|
|
6
|
+
"""
|
|
7
|
+
from typing import Callable, Optional
|
|
8
|
+
|
|
9
|
+
from pal.access_manager.components.access_control import AccessController
|
|
10
|
+
from pal.access_manager.components.base import AccessDecision, AccessRequest, OperationType
|
|
11
|
+
from pal.access_manager.components.intervention import UserInterventionGate
|
|
12
|
+
from pal.access_manager.components.privilege_registry import PrivilegeRegistry
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AccessManagerPipeline:
|
|
16
|
+
def __init__(self, confirmation_handler: Optional[Callable[[AccessRequest], bool]] = None):
|
|
17
|
+
self.registry = PrivilegeRegistry()
|
|
18
|
+
self.controller = AccessController(self.registry)
|
|
19
|
+
self.intervention = UserInterventionGate(confirmation_handler)
|
|
20
|
+
|
|
21
|
+
def assign_privilege_group(self, agent_id: str, group: str) -> None:
|
|
22
|
+
self.registry.assign(agent_id, group)
|
|
23
|
+
|
|
24
|
+
def set_confirmation_handler(self, handler: Callable[[AccessRequest], bool]) -> None:
|
|
25
|
+
self.intervention.set_handler(handler)
|
|
26
|
+
|
|
27
|
+
def check_access(
|
|
28
|
+
self, requesting_agent_id: str, target_agent_id: str, operation: OperationType, resource: str = ""
|
|
29
|
+
) -> AccessDecision:
|
|
30
|
+
"""Privilege check only -- does not trigger user intervention,
|
|
31
|
+
even for a destructive operation. Use authorize() for the full
|
|
32
|
+
gate."""
|
|
33
|
+
request = AccessRequest(
|
|
34
|
+
requesting_agent_id=requesting_agent_id,
|
|
35
|
+
target_agent_id=target_agent_id,
|
|
36
|
+
operation=operation,
|
|
37
|
+
resource=resource,
|
|
38
|
+
)
|
|
39
|
+
return self.controller.check(request)
|
|
40
|
+
|
|
41
|
+
def authorize(
|
|
42
|
+
self, requesting_agent_id: str, target_agent_id: str, operation: OperationType, resource: str = ""
|
|
43
|
+
) -> AccessDecision:
|
|
44
|
+
"""Full authorization: privilege check first, then -- only if
|
|
45
|
+
that passed AND the operation is destructive -- user
|
|
46
|
+
confirmation. Returns one AccessDecision reflecting both
|
|
47
|
+
checks: a privilege-permitted destructive op the user declines
|
|
48
|
+
to confirm comes back allowed=False, not the privilege-only
|
|
49
|
+
result."""
|
|
50
|
+
request = AccessRequest(
|
|
51
|
+
requesting_agent_id=requesting_agent_id,
|
|
52
|
+
target_agent_id=target_agent_id,
|
|
53
|
+
operation=operation,
|
|
54
|
+
resource=resource,
|
|
55
|
+
)
|
|
56
|
+
decision = self.controller.check(request)
|
|
57
|
+
|
|
58
|
+
if not decision.allowed:
|
|
59
|
+
return decision
|
|
60
|
+
|
|
61
|
+
if decision.requires_confirmation and not self.intervention.confirm(request):
|
|
62
|
+
return AccessDecision(
|
|
63
|
+
allowed=False,
|
|
64
|
+
requires_confirmation=True,
|
|
65
|
+
reason="user declined confirmation for an irreversible operation",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return decision
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Output short. No fluff. No intro. No outro.
|
|
2
|
+
|
|
3
|
+
Bad: "Sure! Happy to help. Issue likely caused by..."
|
|
4
|
+
Good: "Bug in auth middleware."
|
|
5
|
+
|
|
6
|
+
Give exactly what user ask. Yes/no question: answer "yes" or "no" only. No extra text.
|
|
7
|
+
|
|
8
|
+
Code: fewest lines possible. No comments unless asked.
|
|
9
|
+
No em dash in output.
|
|
10
|
+
No options unless asked. Give one answer.
|
|
11
|
+
Do not run code unless asked. If output already given, do not run.
|
|
12
|
+
Opinion only if asked or needed.
|
|
13
|
+
No question at end. Never ask "want me to do X or Y?"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Permanent behavior-constraint document, not a text-processing function.
|
|
3
|
+
|
|
4
|
+
The insight: trimming output AFTER the model generates it only saves
|
|
5
|
+
what gets displayed/logged -- the model already paid the generation
|
|
6
|
+
cost (and, for API backends, the billed cost) for every verbose token.
|
|
7
|
+
Instructing the model up front to be terse means fewer output tokens
|
|
8
|
+
get generated in the first place. That's a strictly better lever than
|
|
9
|
+
post-hoc compression, so this is wired in as a permanent addition to
|
|
10
|
+
every system prompt.
|
|
11
|
+
"""
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
_CONSTRAINTS_FILE = Path(__file__).parent / "AGENT_OUTPUT.md"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_agent_constraints() -> str:
|
|
18
|
+
"""Returns the permanent output-behavior constraints as text, ready
|
|
19
|
+
to append to any system prompt."""
|
|
20
|
+
if not _CONSTRAINTS_FILE.exists():
|
|
21
|
+
return ""
|
|
22
|
+
return _CONSTRAINTS_FILE.read_text(encoding="utf-8").strip()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from pal.agent_constraints.components.constraints import load_agent_constraints
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AgentConstraintsPipeline:
|
|
5
|
+
def with_constraints(self, system_prompt: str) -> str:
|
|
6
|
+
"""Merges the permanent output-behavior constraints into a given
|
|
7
|
+
system prompt. Idempotent-ish: if the constraints text is
|
|
8
|
+
already present (e.g. called twice on the same prompt), it
|
|
9
|
+
isn't duplicated."""
|
|
10
|
+
constraints = load_agent_constraints()
|
|
11
|
+
if not constraints:
|
|
12
|
+
return system_prompt
|
|
13
|
+
if constraints in system_prompt:
|
|
14
|
+
return system_prompt
|
|
15
|
+
return f"{system_prompt}\n\n{constraints}"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Module-level convenience, mirroring the old with_agent_constraints()
|
|
19
|
+
# free function so callers that just need the one operation don't have
|
|
20
|
+
# to instantiate the pipeline.
|
|
21
|
+
def with_agent_constraints(system_prompt: str) -> str:
|
|
22
|
+
return AgentConstraintsPipeline().with_constraints(system_prompt)
|
pal/agents/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""The agent application layer. Only agents live here.
|
|
2
|
+
|
|
3
|
+
Everything an agent needs (spec type, base class, ReAct and Reflexion patterns, framework adapters) is in
|
|
4
|
+
`pal.sdk`. Add an agent by adding a NativeAgent subclass with a spec and listing it below.
|
|
5
|
+
"""
|
|
6
|
+
from pal.agents.coding import CodingAgent
|
|
7
|
+
from pal.agents.docs import DocumentationAgent
|
|
8
|
+
from pal.sdk.agent import NativeAgent
|
|
9
|
+
|
|
10
|
+
AGENTS: dict[str, type[NativeAgent]] = {a.spec.id: a for a in (CodingAgent, DocumentationAgent) if a.spec}
|
|
11
|
+
|
|
12
|
+
__all__ = ["AGENTS", "CodingAgent", "DocumentationAgent"]
|
pal/agents/coding.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from pal.sdk.agent import AgentSpec, NativeAgent
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CodingAgent(NativeAgent):
|
|
5
|
+
"""Explains, reviews and writes code from an attached source zip (code graph + read-only source tool)."""
|
|
6
|
+
|
|
7
|
+
spec = AgentSpec(
|
|
8
|
+
id="coding", name="Coding agent",
|
|
9
|
+
description="Explains, reviews and writes code. Attach a source zip and ask about it.",
|
|
10
|
+
role="Role: coding agent. Answer only from the supplied code, graph excerpts and files; say so if something "
|
|
11
|
+
"is not there. Give minimal, correct code in fenced blocks and name the file. Explain only what was asked.",
|
|
12
|
+
key_env="GEMINI_API_KEY_CODING",
|
|
13
|
+
tools=frozenset({"read_source", "search_documents", "read_document", "find_usages", "search_source", "list_project_files",
|
|
14
|
+
"query_code_graph", "build_code_graph", "file_to_markdown"}))
|
pal/agents/docs.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from pal.sdk.agent import AgentSpec, NativeAgent
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DocumentationAgent(NativeAgent):
|
|
5
|
+
"""Writes README, API reference and docstrings strictly from the code or text it is given."""
|
|
6
|
+
|
|
7
|
+
spec = AgentSpec(
|
|
8
|
+
id="docs", name="Documentation agent",
|
|
9
|
+
description="Writes README, API reference and docstrings from code or text you attach.",
|
|
10
|
+
role="Role: documentation agent. Write accurate, concise markdown documentation (README, API reference, "
|
|
11
|
+
"docstrings, changelog) strictly from the supplied code or text. Never invent behaviour: mark unknowns "
|
|
12
|
+
"as TODO. Use headings and short sentences.",
|
|
13
|
+
key_env="GEMINI_API_KEY_DOCS", max_tokens=1536,
|
|
14
|
+
tools=frozenset({"read_source", "search_documents", "read_document", "document_outline", "list_project_files", "search_source",
|
|
15
|
+
"save_file", "query_code_graph", "build_code_graph", "file_to_markdown"}))
|
pal/chat/__init__.py
ADDED
pal/chat/demo.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""The experiment tab's demo: the same 6 messages, sent two ways, measured.
|
|
2
|
+
|
|
3
|
+
naive : what a plain chat client does. Full history every turn (attachments included), the raw
|
|
4
|
+
message, no output rules, every message to the API model.
|
|
5
|
+
pal : ChatService.send() with everything on (routing, input savers, direct answers, document
|
|
6
|
+
index, paging, output rules).
|
|
7
|
+
|
|
8
|
+
Both arms use the same API model and count the tokens the provider itself reports. Latency is
|
|
9
|
+
wall-clock time per turn; Pal's includes the local-model time, so a slow local model can make
|
|
10
|
+
Pal *slower* on some turns, and the summary says so instead of hiding it. Demo turns read and
|
|
11
|
+
write no memory and are not added to the Stats.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import threading
|
|
16
|
+
import time
|
|
17
|
+
import uuid
|
|
18
|
+
from collections import OrderedDict
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from pal.chat.service import ChatService
|
|
23
|
+
from pal.errors import NotFoundError, UserInputError
|
|
24
|
+
from pal.llm_wrapper.components.base import LLMSyscall
|
|
25
|
+
|
|
26
|
+
SURVEY_NAME = "student-survey.md"
|
|
27
|
+
SURVEY = """# Papertrail student survey (120 students, 4 campuses)
|
|
28
|
+
|
|
29
|
+
## Who answered
|
|
30
|
+
- 120 students: 58 undergraduates, 41 master's students, 21 PhD students.
|
|
31
|
+
- 71% take notes on a laptop, 19% on a tablet, 10% on paper only.
|
|
32
|
+
- 64% use more than one note app today; the most common are Google Docs (62%), Notion (38%) and OneNote (27%).
|
|
33
|
+
|
|
34
|
+
## What they dislike about current tools
|
|
35
|
+
- Finding old notes is the top complaint: 57% say they often cannot find a note they wrote last semester.
|
|
36
|
+
- 41% say syncing between phone and laptop fails at least once a month.
|
|
37
|
+
- 33% say the tools are too complicated for lecture notes and slow to open in class.
|
|
38
|
+
- 18% say subscription prices are too high for a student budget.
|
|
39
|
+
|
|
40
|
+
## What they would pay
|
|
41
|
+
- 46% would pay nothing for a note app.
|
|
42
|
+
- 38% would pay up to 3 dollars a month.
|
|
43
|
+
- 12% would pay 3 to 6 dollars a month.
|
|
44
|
+
- 4% would pay more than 6 dollars a month.
|
|
45
|
+
- 72% say a free tier with a paid upgrade is what they prefer.
|
|
46
|
+
|
|
47
|
+
## Where they hear about new apps
|
|
48
|
+
- Instagram: 61%. Student WhatsApp and Telegram groups: 48%.
|
|
49
|
+
- Campus posters and notice boards: 35%. Friends and classmates: 33%.
|
|
50
|
+
- Reddit: 22%. Email newsletters from the university: 14%. TikTok: 12%.
|
|
51
|
+
|
|
52
|
+
## Features they asked for most
|
|
53
|
+
- Search that also finds words inside handwriting and photos of slides: 52%.
|
|
54
|
+
- Works offline in lecture halls: 44%.
|
|
55
|
+
- Share a notebook with a study group: 39%.
|
|
56
|
+
- Turn notes into flashcards: 27%.
|
|
57
|
+
|
|
58
|
+
## Quotes
|
|
59
|
+
- "I write everything down and then never find it again."
|
|
60
|
+
- "I would try anything that opens instantly and does not need a login."
|
|
61
|
+
- "I only download new apps when three friends already use them."
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
STEPS: list[dict[str, Any]] = [
|
|
65
|
+
{"message": "Hi there! So, I was basically wondering if you could please help me plan a small launch for "
|
|
66
|
+
"my new note-taking app called Papertrail. It is really just for students, and I would like to "
|
|
67
|
+
"keep the whole thing under a budget of 500 dollars, if that is at all possible.",
|
|
68
|
+
"route": "api"},
|
|
69
|
+
{"message": "Thanks so much. Here are my survey notes, please just read them and tell me the three most "
|
|
70
|
+
"important things that students say.",
|
|
71
|
+
"attach": True, "route": "api"},
|
|
72
|
+
{"message": "what is 500*0.35", "route": None},
|
|
73
|
+
{"message": "Quick question, what does ROI stand for?", "route": None},
|
|
74
|
+
{"message": "Given the survey notes and the budget, could you please suggest the three channels that I "
|
|
75
|
+
"should focus on, and roughly how to split the 500 dollars between them?",
|
|
76
|
+
"route": "api"},
|
|
77
|
+
{"message": "Great, now please put everything together into a one page launch brief for Papertrail: goal, "
|
|
78
|
+
"audience, budget split, channels, an email subject line, and a first week timeline.",
|
|
79
|
+
"route": "api"},
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
_MAX_RUNS = 20
|
|
83
|
+
_TTL = 3600.0
|
|
84
|
+
_NAIVE_SYSTEM_NOTE = "" # a naive client adds nothing to the base prompt: no output rules
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class _Run:
|
|
89
|
+
id: str
|
|
90
|
+
sid: str
|
|
91
|
+
provider: str
|
|
92
|
+
model: str
|
|
93
|
+
naive_history: list[dict] = field(default_factory=list)
|
|
94
|
+
steps: list[dict] = field(default_factory=list)
|
|
95
|
+
local: dict = field(default_factory=dict)
|
|
96
|
+
created: float = field(default_factory=time.time)
|
|
97
|
+
lock: threading.Lock = field(default_factory=threading.Lock)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _public_steps() -> list[dict]:
|
|
101
|
+
return [{"message": s["message"], "attachment": SURVEY_NAME if s.get("attach") else None} for s in STEPS]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class Demo:
|
|
105
|
+
def __init__(self, service: ChatService):
|
|
106
|
+
self.service = service
|
|
107
|
+
self._runs: OrderedDict[str, _Run] = OrderedDict()
|
|
108
|
+
self._lock = threading.Lock()
|
|
109
|
+
|
|
110
|
+
# -- lifecycle ---------------------------------------------------------------------------------
|
|
111
|
+
def start(self, provider: str, model: str, api_key: str | None) -> dict:
|
|
112
|
+
self.service.api_core(provider, model, api_key) # fail fast on a bad provider, model or missing key
|
|
113
|
+
run = _Run(id=uuid.uuid4().hex, sid=f"demo-{uuid.uuid4().hex[:24]}", provider=provider, model=model,
|
|
114
|
+
local=self.service.local_status())
|
|
115
|
+
with self._lock:
|
|
116
|
+
self._expire()
|
|
117
|
+
self._runs[run.id] = run
|
|
118
|
+
while len(self._runs) > _MAX_RUNS:
|
|
119
|
+
_, old = self._runs.popitem(last=False)
|
|
120
|
+
self.service.reset(old.sid)
|
|
121
|
+
return {"run_id": run.id, "steps": _public_steps(), "local": run.local}
|
|
122
|
+
|
|
123
|
+
def _expire(self) -> None:
|
|
124
|
+
now = time.time()
|
|
125
|
+
for k in [k for k, r in self._runs.items() if now - r.created > _TTL]:
|
|
126
|
+
self.service.reset(self._runs.pop(k).sid)
|
|
127
|
+
|
|
128
|
+
def _get(self, run_id: str) -> _Run:
|
|
129
|
+
with self._lock:
|
|
130
|
+
run = self._runs.get(run_id)
|
|
131
|
+
if run is None:
|
|
132
|
+
raise NotFoundError("demo run not found or expired: start it again")
|
|
133
|
+
return run
|
|
134
|
+
|
|
135
|
+
def cancel(self, run_id: str) -> None:
|
|
136
|
+
with self._lock:
|
|
137
|
+
run = self._runs.pop(run_id, None)
|
|
138
|
+
if run:
|
|
139
|
+
self.service.reset(run.sid)
|
|
140
|
+
|
|
141
|
+
# -- one step ---------------------------------------------------------------------------------------------
|
|
142
|
+
def step(self, run_id: str, api_key: str | None) -> dict:
|
|
143
|
+
run = self._get(run_id)
|
|
144
|
+
with run.lock:
|
|
145
|
+
i = len(run.steps)
|
|
146
|
+
if i >= len(STEPS):
|
|
147
|
+
raise UserInputError("the demo already finished")
|
|
148
|
+
spec = STEPS[i]
|
|
149
|
+
api = self.service.api_core(run.provider, run.model, api_key)
|
|
150
|
+
|
|
151
|
+
# naive arm: full history, raw message, the file re-sent every time it is in the history
|
|
152
|
+
user_text = spec["message"] + (f"\n\n[file: {SURVEY_NAME}]\n{SURVEY}" if spec.get("attach") else "")
|
|
153
|
+
messages = [{"role": "system", "content": self.service.settings.base_system_prompt}]
|
|
154
|
+
messages += run.naive_history + [{"role": "user", "content": user_text}]
|
|
155
|
+
t0 = time.perf_counter()
|
|
156
|
+
try:
|
|
157
|
+
resp = api.chat(LLMSyscall(messages=messages, max_tokens=2048))
|
|
158
|
+
except Exception as e: # noqa: BLE001
|
|
159
|
+
from pal.errors import ProviderError
|
|
160
|
+
secret = getattr(getattr(api, "config", None), "api_key", None) or ""
|
|
161
|
+
text = str(e).replace(secret, "***") if secret else str(e)
|
|
162
|
+
raise ProviderError(f"naive arm: {type(e).__name__}: {text}") from e
|
|
163
|
+
naive = {"input_tokens": resp.prompt_tokens or 0, "output_tokens": resp.completion_tokens or 0,
|
|
164
|
+
"latency": round(time.perf_counter() - t0, 3), "reply": resp.text or ""}
|
|
165
|
+
|
|
166
|
+
# Pal arm: the real service, on its own throwaway session
|
|
167
|
+
attachments = [{"name": SURVEY_NAME, "text": SURVEY, "kind": "document"}] if spec.get("attach") else []
|
|
168
|
+
res = self.service.send(run.sid, run.provider, run.model, api_key, spec["message"], attachments,
|
|
169
|
+
None, route=spec["route"], ephemeral=True)
|
|
170
|
+
pal = {"route": res.handled_by, "reason": res.route_reason, "input_tokens": res.input_tokens,
|
|
171
|
+
"output_tokens": res.output_tokens, "latency": res.latency.get("total", 0.0),
|
|
172
|
+
"latency_local": res.latency.get("local", 0.0), "latency_api": res.latency.get("api", 0.0),
|
|
173
|
+
"tools": [t["name"] for t in res.tools], "reply": res.reply,
|
|
174
|
+
"reduced_tokens_saved": getattr(res.saver, "tokens_saved", 0),
|
|
175
|
+
"history_turns_sent": (res.context or {}).get("turns_sent"),
|
|
176
|
+
"history_tokens_saved": (res.context or {}).get("tokens_saved", 0),
|
|
177
|
+
"document_chunks": (res.documents or {}).get("chunks", 0)}
|
|
178
|
+
|
|
179
|
+
# commit only after both arms succeeded, so a retry of this step stays consistent
|
|
180
|
+
run.naive_history += [{"role": "user", "content": user_text},
|
|
181
|
+
{"role": "assistant", "content": naive["reply"]}]
|
|
182
|
+
row = {"step": i + 1, "message": spec["message"], "naive": naive, "pal": pal}
|
|
183
|
+
run.steps.append(row)
|
|
184
|
+
out: dict[str, Any] = {"row": row, "done": len(run.steps) == len(STEPS)}
|
|
185
|
+
if out["done"]:
|
|
186
|
+
out["summary"] = self._summary(run)
|
|
187
|
+
self.cancel(run.id)
|
|
188
|
+
return out
|
|
189
|
+
|
|
190
|
+
# -- totals ---------------------------------------------------------------------------------------------------
|
|
191
|
+
@staticmethod
|
|
192
|
+
def _pct(saved: float, base: float) -> float:
|
|
193
|
+
return round(100 * saved / base, 1) if base else 0.0
|
|
194
|
+
|
|
195
|
+
def _summary(self, run: _Run) -> dict:
|
|
196
|
+
rows = run.steps
|
|
197
|
+
n_in = sum(r["naive"]["input_tokens"] for r in rows)
|
|
198
|
+
n_out = sum(r["naive"]["output_tokens"] for r in rows)
|
|
199
|
+
m_in = sum(r["pal"]["input_tokens"] for r in rows)
|
|
200
|
+
m_out = sum(r["pal"]["output_tokens"] for r in rows)
|
|
201
|
+
n_lat = sum(r["naive"]["latency"] for r in rows)
|
|
202
|
+
m_lat = sum(r["pal"]["latency"] for r in rows)
|
|
203
|
+
routes = [r["pal"]["route"] for r in rows]
|
|
204
|
+
saved_tokens = (n_in + n_out) - (m_in + m_out)
|
|
205
|
+
saved_lat = n_lat - m_lat
|
|
206
|
+
local_ok = run.local.get("ok")
|
|
207
|
+
notes = []
|
|
208
|
+
if local_ok is False:
|
|
209
|
+
notes.append("The local model was not reachable, so input reduction and local answering did not run. "
|
|
210
|
+
"The saving below comes from direct answers, the output rules and document handling only. "
|
|
211
|
+
"Start Ollama and run the demo again to see the full effect.")
|
|
212
|
+
if saved_lat < 0:
|
|
213
|
+
notes.append("Pal was slower overall: the local model's time is included, and on a slow machine it "
|
|
214
|
+
"can cost more than it saves.")
|
|
215
|
+
notes.append("Pal's output rules make replies shorter, which is part of the token saving and also "
|
|
216
|
+
"changes the style of the answers.")
|
|
217
|
+
return {
|
|
218
|
+
"naive": {"input_tokens": n_in, "output_tokens": n_out, "total_tokens": n_in + n_out,
|
|
219
|
+
"latency": round(n_lat, 2), "api_calls": len(rows)},
|
|
220
|
+
"pal": {"input_tokens": m_in, "output_tokens": m_out, "total_tokens": m_in + m_out,
|
|
221
|
+
"latency": round(m_lat, 2),
|
|
222
|
+
"api_calls": sum(1 for r in routes if r == "api"),
|
|
223
|
+
"local_answers": routes.count("local"), "direct_answers": routes.count("tool"),
|
|
224
|
+
"tool_calls": sum(len(r["pal"]["tools"]) for r in rows)},
|
|
225
|
+
"tokens_saved": saved_tokens, "tokens_saved_pct": self._pct(saved_tokens, n_in + n_out),
|
|
226
|
+
"input_saved": n_in - m_in, "output_saved": n_out - m_out,
|
|
227
|
+
"latency_saved": round(saved_lat, 2), "latency_saved_pct": self._pct(saved_lat, n_lat),
|
|
228
|
+
"api_calls_avoided": len(rows) - sum(1 for r in routes if r == "api"),
|
|
229
|
+
"local": run.local, "notes": notes,
|
|
230
|
+
"result": rows[-1]["pal"]["reply"], "result_naive": rows[-1]["naive"]["reply"],
|
|
231
|
+
}
|