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
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Vector store for agent knowledge bases -- backed by chromadb
|
|
3
|
+
(pip install chromadb) when it's installed, one collection per agent so
|
|
4
|
+
different agents' knowledge bases stay isolated. Falls back to a small
|
|
5
|
+
in-memory store, scored via the same embedding/keyword-overlap
|
|
6
|
+
similarity helpers context_manager and memory already use, when
|
|
7
|
+
chromadb isn't available. Functionally equivalent at the scale this
|
|
8
|
+
project targets -- the real difference is chromadb's store is disk-
|
|
9
|
+
backed (survives a restart), the fallback isn't.
|
|
10
|
+
"""
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from pal.context_manager.utils.relevance import embedding_similarity
|
|
15
|
+
from pal.memory.utils.keyword_overlap import keyword_overlap
|
|
16
|
+
|
|
17
|
+
log = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
import chromadb
|
|
21
|
+
_CHROMADB_AVAILABLE = True
|
|
22
|
+
except ImportError:
|
|
23
|
+
_CHROMADB_AVAILABLE = False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _NaiveVectorStore:
|
|
27
|
+
"""In-process fallback: one dict of doc_id -> (text, metadata) per
|
|
28
|
+
agent, scored at query time rather than indexed ahead of time."""
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
self._collections: dict[str, dict[str, tuple[str, dict]]] = {}
|
|
32
|
+
|
|
33
|
+
def _collection(self, agent_id: str) -> dict:
|
|
34
|
+
return self._collections.setdefault(agent_id, {})
|
|
35
|
+
|
|
36
|
+
def upsert(self, agent_id: str, doc_id: str, text: str, metadata: dict) -> None:
|
|
37
|
+
self._collection(agent_id)[doc_id] = (text, metadata)
|
|
38
|
+
|
|
39
|
+
def delete(self, agent_id: str, doc_id: str) -> None:
|
|
40
|
+
self._collection(agent_id).pop(doc_id, None)
|
|
41
|
+
|
|
42
|
+
def query(self, agent_id: str, query_text: str, top_k: int) -> list[dict]:
|
|
43
|
+
collection = self._collection(agent_id)
|
|
44
|
+
if not collection:
|
|
45
|
+
return []
|
|
46
|
+
doc_ids = list(collection.keys())
|
|
47
|
+
texts = [collection[d][0] for d in doc_ids]
|
|
48
|
+
|
|
49
|
+
sims = embedding_similarity(query_text, texts)
|
|
50
|
+
if all(s == 0.0 for s in sims):
|
|
51
|
+
sims = [keyword_overlap(query_text, t) for t in texts]
|
|
52
|
+
|
|
53
|
+
scored = sorted(zip(doc_ids, sims), key=lambda x: x[1], reverse=True)
|
|
54
|
+
return [
|
|
55
|
+
{"id": doc_id, "text": collection[doc_id][0], "metadata": collection[doc_id][1], "score": score}
|
|
56
|
+
for doc_id, score in scored[:top_k]
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class VectorStoreBackend:
|
|
61
|
+
def __init__(self, persist_directory: Optional[str] = None):
|
|
62
|
+
self.backend = "chromadb" if _CHROMADB_AVAILABLE else "naive_fallback"
|
|
63
|
+
if _CHROMADB_AVAILABLE:
|
|
64
|
+
self._client = (
|
|
65
|
+
chromadb.PersistentClient(path=persist_directory)
|
|
66
|
+
if persist_directory
|
|
67
|
+
else chromadb.EphemeralClient()
|
|
68
|
+
)
|
|
69
|
+
self._collections = {}
|
|
70
|
+
else:
|
|
71
|
+
self._naive = _NaiveVectorStore()
|
|
72
|
+
|
|
73
|
+
def _chroma_collection(self, agent_id: str):
|
|
74
|
+
if agent_id not in self._collections:
|
|
75
|
+
# chromadb collection names have length/charset
|
|
76
|
+
# restrictions -- prefix and truncate to stay valid while
|
|
77
|
+
# keeping per-agent namespacing.
|
|
78
|
+
safe_name = f"agent_{agent_id}".replace(" ", "_")[:63]
|
|
79
|
+
self._collections[agent_id] = self._client.get_or_create_collection(safe_name)
|
|
80
|
+
return self._collections[agent_id]
|
|
81
|
+
|
|
82
|
+
def delete_agent(self, agent_id: str) -> None:
|
|
83
|
+
"""Drop an agent's whole collection."""
|
|
84
|
+
if self.backend == "chromadb":
|
|
85
|
+
self._collections.pop(agent_id, None)
|
|
86
|
+
try:
|
|
87
|
+
self._client.delete_collection(f"agent_{agent_id}".replace(" ", "_")[:63])
|
|
88
|
+
except Exception: # noqa: BLE001 (collection was never created)
|
|
89
|
+
log.debug("no collection to delete for %s", agent_id)
|
|
90
|
+
else:
|
|
91
|
+
self._naive._collections.pop(agent_id, None)
|
|
92
|
+
|
|
93
|
+
def upsert(self, agent_id: str, doc_id: str, text: str, metadata: dict = None) -> None:
|
|
94
|
+
metadata = metadata or {}
|
|
95
|
+
if self.backend == "chromadb":
|
|
96
|
+
self._chroma_collection(agent_id).upsert(
|
|
97
|
+
ids=[doc_id], documents=[text], metadatas=[metadata] if metadata else None
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
self._naive.upsert(agent_id, doc_id, text, metadata)
|
|
101
|
+
|
|
102
|
+
def delete(self, agent_id: str, doc_id: str) -> None:
|
|
103
|
+
if self.backend == "chromadb":
|
|
104
|
+
self._chroma_collection(agent_id).delete(ids=[doc_id])
|
|
105
|
+
else:
|
|
106
|
+
self._naive.delete(agent_id, doc_id)
|
|
107
|
+
|
|
108
|
+
def query(self, agent_id: str, query_text: str, top_k: int = 3) -> list[dict]:
|
|
109
|
+
if self.backend == "chromadb":
|
|
110
|
+
collection = self._chroma_collection(agent_id)
|
|
111
|
+
if collection.count() == 0:
|
|
112
|
+
return []
|
|
113
|
+
result = collection.query(query_texts=[query_text], n_results=min(top_k, collection.count()))
|
|
114
|
+
ids = result.get("ids", [[]])[0]
|
|
115
|
+
docs = result.get("documents", [[]])[0]
|
|
116
|
+
metas = result.get("metadatas", [[]])[0] or [{}] * len(ids)
|
|
117
|
+
dists = result.get("distances", [[]])[0] or [None] * len(ids)
|
|
118
|
+
return [
|
|
119
|
+
{"id": doc_id, "text": text, "metadata": meta, "score": (1 - dist) if dist is not None else None}
|
|
120
|
+
for doc_id, text, meta, dist in zip(ids, docs, metas, dists)
|
|
121
|
+
]
|
|
122
|
+
return self._naive.query(agent_id, query_text, top_k)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Storage Manager (AIOS section 3.6): persistent storage for agents --
|
|
3
|
+
files/knowledge bases they depend on, and durable storage for the
|
|
4
|
+
memory manager's RAM-to-disk swaps -- backed by local files
|
|
5
|
+
(LocalFileBackend) and a vector database (VectorStoreBackend, chromadb
|
|
6
|
+
when installed).
|
|
7
|
+
|
|
8
|
+
Two entry points into the same underlying storage, matching the two
|
|
9
|
+
callers section 3.6 describes:
|
|
10
|
+
|
|
11
|
+
- **Direct calls** (read/write/delete/list_keys/vector_upsert/
|
|
12
|
+
vector_query) -- what the memory manager uses when it swaps a note to
|
|
13
|
+
disk, scoped by the agent_id the memory manager passes in.
|
|
14
|
+
- **execute(StorageSyscall)**, reachable either directly or via the
|
|
15
|
+
queue (submit/tick/drain) -- what an agent's own SDK calls
|
|
16
|
+
(utils/sdk.py) go through: "the agent calls the storage API in the
|
|
17
|
+
SDK, which is further converted into storage-related system calls
|
|
18
|
+
and put into the storage queue... the storage manager then processes
|
|
19
|
+
the requests in the queue."
|
|
20
|
+
|
|
21
|
+
Both paths converge on execute()/the same backends -- there's exactly
|
|
22
|
+
one storage manager per process, not a separate implementation per
|
|
23
|
+
caller.
|
|
24
|
+
|
|
25
|
+
When an AccessManagerPipeline (AIOS section 3.8) is supplied, execute()
|
|
26
|
+
authorizes every request before performing it: a cross-agent request
|
|
27
|
+
(syscall.requesting_agent_id differs from syscall.agent_id, the
|
|
28
|
+
resource owner) is checked against the privilege-group hashmap, and any
|
|
29
|
+
destructive operation -- DELETE, or a WRITE that overwrites an existing
|
|
30
|
+
key -- goes through user intervention regardless of who's making the
|
|
31
|
+
call. Without an access_manager, every request is permitted (the
|
|
32
|
+
pre-3.8 behavior), matching how the direct read/write/etc. calls above
|
|
33
|
+
still bypass access control entirely -- those are for the memory
|
|
34
|
+
manager's own swaps, which are not the cross-agent case 3.8 regulates.
|
|
35
|
+
"""
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Any, Optional
|
|
38
|
+
|
|
39
|
+
from pal.access_manager.components.base import OperationType
|
|
40
|
+
from pal.access_manager.pipeline import AccessManagerPipeline
|
|
41
|
+
from pal.paths import data_dir
|
|
42
|
+
from pal.storage_manager.components.base import StorageOp, StorageResponse, StorageSyscall
|
|
43
|
+
from pal.storage_manager.components.local_file_backend import LocalFileBackend
|
|
44
|
+
from pal.storage_manager.components.storage_queue import StorageSyscallQueue
|
|
45
|
+
from pal.storage_manager.components.vector_store_backend import VectorStoreBackend
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class StorageManagerPipeline:
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
root: Optional[str] = None,
|
|
52
|
+
vector_persist_directory: Optional[str] = None,
|
|
53
|
+
access_manager: Optional[AccessManagerPipeline] = None,
|
|
54
|
+
):
|
|
55
|
+
root_path = Path(root) if root else data_dir("storage")
|
|
56
|
+
self.files = LocalFileBackend(root_path / "files")
|
|
57
|
+
self.vectors = VectorStoreBackend(persist_directory=vector_persist_directory)
|
|
58
|
+
self.queue = StorageSyscallQueue(self)
|
|
59
|
+
self.access_manager = access_manager
|
|
60
|
+
|
|
61
|
+
# -- direct calls (memory manager swaps, or any in-process caller) --------
|
|
62
|
+
|
|
63
|
+
def read(self, agent_id: str, key: str) -> Optional[Any]:
|
|
64
|
+
return self.files.read(agent_id, key)
|
|
65
|
+
|
|
66
|
+
def write(self, agent_id: str, key: str, data: Any) -> None:
|
|
67
|
+
self.files.write(agent_id, key, data)
|
|
68
|
+
|
|
69
|
+
def delete(self, agent_id: str, key: str) -> None:
|
|
70
|
+
self.files.delete(agent_id, key)
|
|
71
|
+
|
|
72
|
+
def exists(self, agent_id: str, key: str) -> bool:
|
|
73
|
+
return self.files.exists(agent_id, key)
|
|
74
|
+
|
|
75
|
+
def list_keys(self, agent_id: str, prefix: str = "") -> list[str]:
|
|
76
|
+
return self.files.list_keys(agent_id, prefix=prefix)
|
|
77
|
+
|
|
78
|
+
def vector_upsert(self, agent_id: str, doc_id: str, text: str, metadata: dict = None) -> None:
|
|
79
|
+
self.vectors.upsert(agent_id, doc_id, text, metadata or {})
|
|
80
|
+
|
|
81
|
+
def vector_query(self, agent_id: str, query_text: str, top_k: int = 3) -> list[dict]:
|
|
82
|
+
return self.vectors.query(agent_id, query_text, top_k=top_k)
|
|
83
|
+
|
|
84
|
+
# -- system-call entry point (agent SDK calls, direct or via queue) -------
|
|
85
|
+
|
|
86
|
+
def _operation_type(self, syscall: StorageSyscall) -> OperationType:
|
|
87
|
+
if syscall.op == StorageOp.DELETE:
|
|
88
|
+
return OperationType.DELETE
|
|
89
|
+
if syscall.op in (StorageOp.READ, StorageOp.LIST, StorageOp.VECTOR_QUERY):
|
|
90
|
+
return OperationType.READ
|
|
91
|
+
if syscall.op == StorageOp.WRITE:
|
|
92
|
+
# Overwriting an existing key destroys what was there --
|
|
93
|
+
# treat it as destructive (user-intervention-gated) the same
|
|
94
|
+
# as an explicit delete; a write to a fresh key is not.
|
|
95
|
+
return OperationType.OVERWRITE if self.exists(syscall.agent_id, syscall.key) else OperationType.WRITE
|
|
96
|
+
return OperationType.WRITE # VECTOR_UPSERT
|
|
97
|
+
|
|
98
|
+
def execute(self, syscall: StorageSyscall) -> StorageResponse:
|
|
99
|
+
if self.access_manager is not None:
|
|
100
|
+
requesting = syscall.requesting_agent_id or syscall.agent_id
|
|
101
|
+
decision = self.access_manager.authorize(
|
|
102
|
+
requesting_agent_id=requesting,
|
|
103
|
+
target_agent_id=syscall.agent_id,
|
|
104
|
+
operation=self._operation_type(syscall),
|
|
105
|
+
resource=syscall.key or syscall.query_text or "",
|
|
106
|
+
)
|
|
107
|
+
if not decision.allowed:
|
|
108
|
+
return StorageResponse(ok=False, error=f"access denied: {decision.reason}")
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
if syscall.op == StorageOp.READ:
|
|
112
|
+
return StorageResponse(ok=True, data=self.read(syscall.agent_id, syscall.key))
|
|
113
|
+
if syscall.op == StorageOp.WRITE:
|
|
114
|
+
self.write(syscall.agent_id, syscall.key, syscall.data)
|
|
115
|
+
return StorageResponse(ok=True)
|
|
116
|
+
if syscall.op == StorageOp.DELETE:
|
|
117
|
+
self.delete(syscall.agent_id, syscall.key)
|
|
118
|
+
return StorageResponse(ok=True)
|
|
119
|
+
if syscall.op == StorageOp.LIST:
|
|
120
|
+
return StorageResponse(ok=True, data=self.list_keys(syscall.agent_id, prefix=syscall.key or ""))
|
|
121
|
+
if syscall.op == StorageOp.VECTOR_UPSERT:
|
|
122
|
+
self.vector_upsert(syscall.agent_id, syscall.key, syscall.data, syscall.metadata)
|
|
123
|
+
return StorageResponse(ok=True)
|
|
124
|
+
if syscall.op == StorageOp.VECTOR_QUERY:
|
|
125
|
+
return StorageResponse(
|
|
126
|
+
ok=True,
|
|
127
|
+
data=self.vector_query(syscall.agent_id, syscall.query_text, syscall.top_k),
|
|
128
|
+
)
|
|
129
|
+
return StorageResponse(ok=False, error=f"unknown storage op: {syscall.op}")
|
|
130
|
+
except Exception as e:
|
|
131
|
+
return StorageResponse(ok=False, error=str(e))
|
|
132
|
+
|
|
133
|
+
# -- queue-driven path -- "put into the storage queue by the scheduler" --
|
|
134
|
+
|
|
135
|
+
def submit(self, syscall: StorageSyscall) -> str:
|
|
136
|
+
return self.queue.submit(syscall)
|
|
137
|
+
|
|
138
|
+
def tick(self):
|
|
139
|
+
return self.queue.process_next()
|
|
140
|
+
|
|
141
|
+
def drain(self, max_iterations: int = 10_000):
|
|
142
|
+
return self.queue.drain(max_iterations=max_iterations)
|
|
143
|
+
|
|
144
|
+
def get_result(self, request_id: str):
|
|
145
|
+
return self.queue.get_result(request_id)
|
|
146
|
+
|
|
147
|
+
def forget(self, request_id: str) -> None:
|
|
148
|
+
self.queue.forget(request_id)
|
|
149
|
+
|
|
150
|
+
def purge_agent(self, agent_id: str) -> None:
|
|
151
|
+
"""Delete every file and vector stored for an agent (used when a chat session ends)."""
|
|
152
|
+
self.files.purge(agent_id)
|
|
153
|
+
self.vectors.delete_agent(agent_id)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Adapter presenting the same narrow interface
|
|
3
|
+
pal.memory.components.storage_manager.StorageManager exposes
|
|
4
|
+
(write/read/delete/exists/list_ids/read_all, all keyed by MemoryNote.id)
|
|
5
|
+
but backed by the general-purpose StorageManagerPipeline, scoped to one
|
|
6
|
+
agent_id.
|
|
7
|
+
|
|
8
|
+
This is the actual integration point for "when the agent's memory
|
|
9
|
+
usage exceeds the allocated limit, the memory manager calls the storage
|
|
10
|
+
manager to swap the data into the disk... the storage manager reads and
|
|
11
|
+
writes data based on the agent ID passed from the memory manager":
|
|
12
|
+
MemoryManager doesn't know or care which storage implementation sits
|
|
13
|
+
behind this interface, only that it has one -- so it works identically
|
|
14
|
+
whether it's given this adapter (backed by the shared, agent-scoped
|
|
15
|
+
StorageManagerPipeline) or the standalone pal.memory.components.
|
|
16
|
+
storage_manager.StorageManager (a private, unscoped JSON-per-note
|
|
17
|
+
store, still the default when no agent_id/storage_manager is supplied).
|
|
18
|
+
"""
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
from pal.memory.components.note import MemoryNote
|
|
22
|
+
from pal.storage_manager.pipeline import StorageManagerPipeline
|
|
23
|
+
|
|
24
|
+
_MEMORY_PREFIX = "memory/"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AgentScopedMemoryStorage:
|
|
28
|
+
def __init__(self, storage_manager: StorageManagerPipeline, agent_id: str):
|
|
29
|
+
self.storage_manager = storage_manager
|
|
30
|
+
self.agent_id = agent_id
|
|
31
|
+
|
|
32
|
+
def _key(self, note_id: str) -> str:
|
|
33
|
+
return f"{_MEMORY_PREFIX}{note_id}.json"
|
|
34
|
+
|
|
35
|
+
def write(self, note: MemoryNote) -> None:
|
|
36
|
+
self.storage_manager.write(self.agent_id, self._key(note.id), note.to_dict())
|
|
37
|
+
|
|
38
|
+
def read(self, note_id: str) -> Optional[MemoryNote]:
|
|
39
|
+
data = self.storage_manager.read(self.agent_id, self._key(note_id))
|
|
40
|
+
if data is None:
|
|
41
|
+
return None
|
|
42
|
+
return MemoryNote.from_dict(data)
|
|
43
|
+
|
|
44
|
+
def delete(self, note_id: str) -> None:
|
|
45
|
+
self.storage_manager.delete(self.agent_id, self._key(note_id))
|
|
46
|
+
|
|
47
|
+
def exists(self, note_id: str) -> bool:
|
|
48
|
+
return self.storage_manager.exists(self.agent_id, self._key(note_id))
|
|
49
|
+
|
|
50
|
+
def list_ids(self) -> list[str]:
|
|
51
|
+
keys = self.storage_manager.list_keys(self.agent_id, prefix=_MEMORY_PREFIX)
|
|
52
|
+
return [k[len(_MEMORY_PREFIX):-len(".json")] for k in keys if k.endswith(".json")]
|
|
53
|
+
|
|
54
|
+
def read_all(self) -> list[MemoryNote]:
|
|
55
|
+
notes = []
|
|
56
|
+
for note_id in self.list_ids():
|
|
57
|
+
note = self.read(note_id)
|
|
58
|
+
if note is not None:
|
|
59
|
+
notes.append(note)
|
|
60
|
+
return notes
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Storage SDK: what an agent calls to read/write its own persistent data
|
|
3
|
+
during runtime -- distinct from the memory manager's own direct calls
|
|
4
|
+
into the same storage manager (see utils/memory_adapter.py).
|
|
5
|
+
|
|
6
|
+
"The agent calls the storage API in the SDK, which is further
|
|
7
|
+
converted into storage-related system calls and put into the storage
|
|
8
|
+
queue by the scheduler. The storage manager then processes the
|
|
9
|
+
requests in the queue." read()/write()/etc. here build a StorageSyscall
|
|
10
|
+
and submit it to the shared StorageManagerPipeline's queue, then drain
|
|
11
|
+
that queue until this specific request completes -- so the call still
|
|
12
|
+
looks synchronous to the agent even though it went through the same
|
|
13
|
+
centralized queue every other storage request goes through, the same
|
|
14
|
+
way Pipeline._dispatch_chat drains the LLM syscall scheduler for its
|
|
15
|
+
own request in orchestrator/components/syscall_scheduler.py.
|
|
16
|
+
|
|
17
|
+
read_from()/write_from()/delete_from() are the cross-agent path AIOS
|
|
18
|
+
3.8's Access Manager regulates: they set requesting_agent_id to this
|
|
19
|
+
SDK's own agent_id while targeting a DIFFERENT agent's resource, so if
|
|
20
|
+
the StorageManagerPipeline has an access_manager wired in, that request
|
|
21
|
+
gets checked against the privilege-group hashmap (and, for delete/
|
|
22
|
+
overwrite, gated behind user intervention) before it's allowed to
|
|
23
|
+
proceed. Plain read()/write()/delete() never trigger a cross-agent
|
|
24
|
+
check -- they always act on this SDK's own agent_id as both requester
|
|
25
|
+
and target.
|
|
26
|
+
"""
|
|
27
|
+
from typing import Any, Optional
|
|
28
|
+
|
|
29
|
+
from pal.storage_manager.components.base import StorageOp, StorageSyscall
|
|
30
|
+
from pal.storage_manager.pipeline import StorageManagerPipeline
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class StorageSDK:
|
|
34
|
+
def __init__(self, agent_id: str, storage_manager: StorageManagerPipeline):
|
|
35
|
+
self.agent_id = agent_id
|
|
36
|
+
self.storage_manager = storage_manager
|
|
37
|
+
|
|
38
|
+
def _submit_and_wait(self, syscall: StorageSyscall):
|
|
39
|
+
request_id = self.storage_manager.submit(syscall)
|
|
40
|
+
try:
|
|
41
|
+
req = self.storage_manager.get_result(request_id)
|
|
42
|
+
while req.status.value == "queued":
|
|
43
|
+
self.storage_manager.tick()
|
|
44
|
+
req = self.storage_manager.get_result(request_id)
|
|
45
|
+
if req.status.value == "error":
|
|
46
|
+
error = req.response.error if req.response else "unknown error"
|
|
47
|
+
raise RuntimeError(f"storage syscall failed: {error}")
|
|
48
|
+
return req.response
|
|
49
|
+
finally:
|
|
50
|
+
self.storage_manager.forget(request_id)
|
|
51
|
+
|
|
52
|
+
def read(self, key: str) -> Optional[Any]:
|
|
53
|
+
resp = self._submit_and_wait(StorageSyscall(agent_id=self.agent_id, op=StorageOp.READ, key=key))
|
|
54
|
+
return resp.data
|
|
55
|
+
|
|
56
|
+
def write(self, key: str, data: Any) -> None:
|
|
57
|
+
self._submit_and_wait(StorageSyscall(agent_id=self.agent_id, op=StorageOp.WRITE, key=key, data=data))
|
|
58
|
+
|
|
59
|
+
def delete(self, key: str) -> None:
|
|
60
|
+
self._submit_and_wait(StorageSyscall(agent_id=self.agent_id, op=StorageOp.DELETE, key=key))
|
|
61
|
+
|
|
62
|
+
def list_keys(self, prefix: str = "") -> list[str]:
|
|
63
|
+
resp = self._submit_and_wait(StorageSyscall(agent_id=self.agent_id, op=StorageOp.LIST, key=prefix))
|
|
64
|
+
return resp.data
|
|
65
|
+
|
|
66
|
+
def vector_upsert(self, doc_id: str, text: str, metadata: dict = None) -> None:
|
|
67
|
+
self._submit_and_wait(StorageSyscall(
|
|
68
|
+
agent_id=self.agent_id, op=StorageOp.VECTOR_UPSERT, key=doc_id, data=text, metadata=metadata or {},
|
|
69
|
+
))
|
|
70
|
+
|
|
71
|
+
def vector_query(self, query_text: str, top_k: int = 3) -> list[dict]:
|
|
72
|
+
resp = self._submit_and_wait(StorageSyscall(
|
|
73
|
+
agent_id=self.agent_id, op=StorageOp.VECTOR_QUERY, query_text=query_text, top_k=top_k,
|
|
74
|
+
))
|
|
75
|
+
return resp.data
|
|
76
|
+
|
|
77
|
+
# -- cross-agent access, subject to the Access Manager (AIOS 3.8) ---------
|
|
78
|
+
|
|
79
|
+
def read_from(self, target_agent_id: str, key: str) -> Optional[Any]:
|
|
80
|
+
resp = self._submit_and_wait(StorageSyscall(
|
|
81
|
+
agent_id=target_agent_id, requesting_agent_id=self.agent_id, op=StorageOp.READ, key=key,
|
|
82
|
+
))
|
|
83
|
+
return resp.data
|
|
84
|
+
|
|
85
|
+
def write_to(self, target_agent_id: str, key: str, data: Any) -> None:
|
|
86
|
+
self._submit_and_wait(StorageSyscall(
|
|
87
|
+
agent_id=target_agent_id, requesting_agent_id=self.agent_id, op=StorageOp.WRITE, key=key, data=data,
|
|
88
|
+
))
|
|
89
|
+
|
|
90
|
+
def delete_from(self, target_agent_id: str, key: str) -> None:
|
|
91
|
+
self._submit_and_wait(StorageSyscall(
|
|
92
|
+
agent_id=target_agent_id, requesting_agent_id=self.agent_id, op=StorageOp.DELETE, key=key,
|
|
93
|
+
))
|
pal/studio/__init__.py
ADDED
|
File without changes
|
pal/studio/builder.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""One build: the plan's steps, one after another, each a request to the model, each reply read for files, checked, and saved under the policy.
|
|
2
|
+
|
|
3
|
+
Everything that happens is reported as an event (`emit(kind, text, **data)`), so the page can show the work as it happens. Nothing the model wrote is
|
|
4
|
+
ever run: files are read, checked for mistakes, and written, if the policy allows.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pal.studio.catalog import ROLES
|
|
13
|
+
from pal.studio.checks import check_project
|
|
14
|
+
from pal.studio.policy import Policy
|
|
15
|
+
from pal.terminal.files import parse_files
|
|
16
|
+
|
|
17
|
+
FORMAT = ("Reply with the files you create or change. Write each file as a line `### FILE: path/name.ext` followed by its COMPLETE content in a code block. "
|
|
18
|
+
"No partial files, no '...' placeholders. After the files you may add one line starting with NOTE: if something needs saying.")
|
|
19
|
+
KIND_RULES = {
|
|
20
|
+
"web_app": "It is a web page: plain HTML, CSS and JavaScript that works when index.html is opened in a browser. No build step, no frameworks to install, no external URLs or CDNs. Start with index.html.",
|
|
21
|
+
"cli_tool": "It is a command-line program in Python 3 using only the standard library. Start with main.py.",
|
|
22
|
+
"script": "It is a Python 3 script using only the standard library. Start with main.py.",
|
|
23
|
+
"library": "It is a small Python 3 library using only the standard library, with a clear public API.",
|
|
24
|
+
"document": "It is a document: write it as Markdown files.",
|
|
25
|
+
"other": "Use plain files that need nothing to be installed.",
|
|
26
|
+
}
|
|
27
|
+
MAX_REPAIRS = 1
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def default_name(plan: dict, step: dict) -> str:
|
|
31
|
+
if step["role"] == "docs":
|
|
32
|
+
return "README.md"
|
|
33
|
+
if step["role"] == "tester":
|
|
34
|
+
return "test_main.py"
|
|
35
|
+
return {"web_app": "index.html", "cli_tool": "main.py", "script": "main.py", "library": "main.py", "document": "README.md"}.get(plan["product"]["kind"], "main.txt")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def first_message(plan: dict, step: dict, with_references: bool) -> str:
|
|
39
|
+
team = ", ".join(ROLES[r].name for r in plan["roles"])
|
|
40
|
+
p = plan["product"]
|
|
41
|
+
return "\n".join([
|
|
42
|
+
f"We are building: {p['name']}. {p['summary']}",
|
|
43
|
+
KIND_RULES.get(p["kind"], KIND_RULES["other"]),
|
|
44
|
+
f"The team: {team}. Each step is done by one of them, and every file must work with the files already written (same names, same functions).",
|
|
45
|
+
"The reference files the person attached are available to you." if with_references else "",
|
|
46
|
+
FORMAT, "",
|
|
47
|
+
f"Step 1 ({ROLES[step['role']].name}): {step['task']}", ROLES[step["role"]].prompt])
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def step_message(step: dict, i: int, n: int) -> str:
|
|
51
|
+
return (f"Step {i} of {n} ({ROLES[step['role']].name}): {step['task']}\n{ROLES[step['role']].prompt}\n"
|
|
52
|
+
"Keep file names and functions consistent with the files already written. " + FORMAT)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Build:
|
|
56
|
+
def __init__(self, plan: dict, client, workspace: Path | None, policy: Policy, emit: Callable, cancelled: Callable[[], bool] = lambda: False,
|
|
57
|
+
references: list[dict] | None = None):
|
|
58
|
+
self.plan, self.client, self.workspace, self.policy, self.emit, self.cancelled = plan, client, workspace, policy, emit, cancelled
|
|
59
|
+
self.references = references if plan["permissions"].get("read_references") else []
|
|
60
|
+
self.files: dict[str, str] = {} # everything the build wrote (or would have written, in a dry run)
|
|
61
|
+
self.totals: dict[str, Any] = {"calls": 0, "prompt_tokens": 0, "completion_tokens": 0, "main_tokens": 0, "helper_tokens": 0, "seconds": 0.0,
|
|
62
|
+
"files": 0, "blocked": 0, "problems_left": 0, "steps_done": 0, "steps": len(plan["steps"]), "stopped": ""}
|
|
63
|
+
|
|
64
|
+
# -- one request to the model ----------------------------------------------------------------------------------------
|
|
65
|
+
def _ask(self, message: str, label: str, refs: list[dict] | None = None) -> str:
|
|
66
|
+
reply = self.client.ask(message, refs)
|
|
67
|
+
t = self.totals
|
|
68
|
+
t["calls"] += max(reply.calls, 1)
|
|
69
|
+
t["prompt_tokens"] += reply.prompt_tokens
|
|
70
|
+
t["completion_tokens"] += reply.completion_tokens
|
|
71
|
+
t["main_tokens"] += reply.prompt_tokens + reply.completion_tokens
|
|
72
|
+
t["helper_tokens"] += reply.helper_tokens
|
|
73
|
+
t["seconds"] = round(t["seconds"] + reply.seconds, 3)
|
|
74
|
+
self.emit("model_call", f"{label}: the model read {reply.prompt_tokens:,} tokens and wrote {reply.completion_tokens:,} in {reply.seconds:.1f}s"
|
|
75
|
+
+ (f" (Pal's helper used {reply.helper_tokens:,} more)" if reply.helper_tokens else ""),
|
|
76
|
+
prompt_tokens=reply.prompt_tokens, completion_tokens=reply.completion_tokens, helper_tokens=reply.helper_tokens, seconds=round(reply.seconds, 2))
|
|
77
|
+
return reply.text
|
|
78
|
+
|
|
79
|
+
# -- reading and saving the files in a reply -----------------------------------------------------------------------
|
|
80
|
+
def _apply(self, reply: str, step: dict) -> set[str]:
|
|
81
|
+
parsed, unnamed = parse_files(reply, default_name(self.plan, step))
|
|
82
|
+
changed: set[str] = set()
|
|
83
|
+
if unnamed:
|
|
84
|
+
self.emit("warning", f"{unnamed} code block(s) had no file name and were skipped")
|
|
85
|
+
for pf in parsed:
|
|
86
|
+
d = self.policy.check_write(pf.path, pf.content)
|
|
87
|
+
if d.allowed:
|
|
88
|
+
existed = d.path.lower() in {p.lower() for p in self.files}
|
|
89
|
+
self.files[d.path] = pf.content
|
|
90
|
+
if self.workspace is not None:
|
|
91
|
+
target = self.workspace / d.path
|
|
92
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
target.write_text(pf.content, encoding="utf-8", newline="\n")
|
|
94
|
+
changed.add(d.path)
|
|
95
|
+
self.totals["files"] = len(self.files)
|
|
96
|
+
self.emit("file", f"{'changed' if existed else 'wrote'} {d.path} ({len(pf.content.encode('utf-8')):,} bytes)", path=d.path, action="changed" if existed else "created")
|
|
97
|
+
elif d.path and "dry run" in d.reason:
|
|
98
|
+
self.files[d.path] = pf.content
|
|
99
|
+
changed.add(d.path)
|
|
100
|
+
self.totals["files"] = len(self.files)
|
|
101
|
+
self.emit("file", f"would write {d.path} (dry run: not saved)", path=d.path, action="dry")
|
|
102
|
+
else:
|
|
103
|
+
self.totals["blocked"] += 1
|
|
104
|
+
self.emit("blocked", f"did not save {d.path or 'a file'}: {d.reason}", path=d.path, reason=d.reason)
|
|
105
|
+
return changed
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def _describe(problems: dict[str, list[str]]) -> str:
|
|
109
|
+
return "\n".join(f"- {path}: {'; '.join(p)}" for path, p in problems.items())
|
|
110
|
+
|
|
111
|
+
# -- the build ---------------------------------------------------------------------------------------------------------
|
|
112
|
+
def run(self) -> dict:
|
|
113
|
+
steps, n = self.plan["steps"], len(self.plan["steps"])
|
|
114
|
+
for i, step in enumerate(steps, 1):
|
|
115
|
+
if self.cancelled():
|
|
116
|
+
self.totals["stopped"] = "cancelled"
|
|
117
|
+
self.emit("cancelled", "stopped by you")
|
|
118
|
+
return self.totals
|
|
119
|
+
role = ROLES[step["role"]]
|
|
120
|
+
self.emit("step_start", f"Step {i} of {n}: {role.name}: {step['task']}", step=i, total=n, role=step["role"])
|
|
121
|
+
first = i == 1
|
|
122
|
+
msg = first_message(self.plan, step, bool(self.references)) if first else step_message(step, i, n)
|
|
123
|
+
try:
|
|
124
|
+
reply = self._ask(msg, f"step {i}", self.references if first else None)
|
|
125
|
+
except Exception as e: # noqa: BLE001 (the model failed: say so, and stop cleanly)
|
|
126
|
+
self.totals["stopped"] = "error"
|
|
127
|
+
self.emit("error", f"the model failed on step {i}: {type(e).__name__}: {str(e)[:200]}")
|
|
128
|
+
return self.totals
|
|
129
|
+
changed = self._apply(reply, step)
|
|
130
|
+
problems = check_project(self.files, only=changed, links=False)
|
|
131
|
+
repairs = 0
|
|
132
|
+
while problems and repairs < MAX_REPAIRS and not self.cancelled():
|
|
133
|
+
self.emit("check", "found problems: " + "; ".join(f"{p}: {', '.join(v)}" for p, v in list(problems.items())[:3]), problems=problems)
|
|
134
|
+
repairs += 1
|
|
135
|
+
try:
|
|
136
|
+
fix = self._ask("These problems were found in the files:\n" + self._describe(problems) + "\nReply with the corrected COMPLETE files only. " + FORMAT, f"step {i} fix")
|
|
137
|
+
except Exception as e: # noqa: BLE001
|
|
138
|
+
self.emit("error", f"the fix request failed: {type(e).__name__}")
|
|
139
|
+
break
|
|
140
|
+
changed |= self._apply(fix, step)
|
|
141
|
+
problems = check_project(self.files, only=changed, links=False)
|
|
142
|
+
if problems:
|
|
143
|
+
self.emit("check", "problems still there after one fix: " + "; ".join(f"{p}: {', '.join(v)}" for p, v in list(problems.items())[:3]), problems=problems)
|
|
144
|
+
else:
|
|
145
|
+
self.emit("check", "no problems found in the files")
|
|
146
|
+
self.totals["steps_done"] = i
|
|
147
|
+
self.emit("step_done", f"Step {i} done: {len(self.files)} file(s) so far", step=i)
|
|
148
|
+
self._final_check(steps[-1] if steps else None)
|
|
149
|
+
return self.totals
|
|
150
|
+
|
|
151
|
+
def _final_check(self, step: dict | None) -> None:
|
|
152
|
+
"""When every step is done: do the files fit together (does every page link to files that exist)? One fix is allowed, then the result is recorded."""
|
|
153
|
+
if step is None or self.cancelled() or self.totals["stopped"]:
|
|
154
|
+
return
|
|
155
|
+
problems = check_project(self.files)
|
|
156
|
+
if problems:
|
|
157
|
+
self.emit("check", "project check, found: " + "; ".join(f"{p}: {', '.join(v)}" for p, v in list(problems.items())[:3]), problems=problems)
|
|
158
|
+
try:
|
|
159
|
+
fix = self._ask("These problems were found in the finished project:\n" + self._describe(problems) + "\nReply with the missing or corrected COMPLETE files only. " + FORMAT, "final fix")
|
|
160
|
+
self._apply(fix, step)
|
|
161
|
+
except Exception as e: # noqa: BLE001
|
|
162
|
+
self.emit("error", f"the final fix request failed: {type(e).__name__}")
|
|
163
|
+
problems = check_project(self.files)
|
|
164
|
+
self.totals["problems_left"] = sum(len(v) for v in problems.values())
|
|
165
|
+
self.emit("check", "project check: the files fit together" if not problems else f"project check: {self.totals['problems_left']} problem(s) left: "
|
|
166
|
+
+ "; ".join(f"{p}: {', '.join(v)}" for p, v in list(problems.items())[:3]), problems=problems)
|