polymath-agent 0.4.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.
- polymath/__init__.py +2 -0
- polymath/adapters/__init__.py +7 -0
- polymath/adapters/base.py +175 -0
- polymath/adapters/claude.py +280 -0
- polymath/adapters/gemini.py +186 -0
- polymath/adapters/ollama.py +117 -0
- polymath/adapters/openai_adapter.py +168 -0
- polymath/bootstrap.py +159 -0
- polymath/command_registry.py +41 -0
- polymath/command_service.py +572 -0
- polymath/compressor.py +90 -0
- polymath/config.py +293 -0
- polymath/context_manager.py +76 -0
- polymath/context_store.py +336 -0
- polymath/detector.py +442 -0
- polymath/domain.py +78 -0
- polymath/execution_service.py +325 -0
- polymath/main.py +1293 -0
- polymath/memory/__init__.py +15 -0
- polymath/memory/chunker.py +6 -0
- polymath/memory/embedder.py +179 -0
- polymath/memory/migrate.py +2 -0
- polymath/memory/retriever.py +2 -0
- polymath/memory/store.py +9 -0
- polymath/memory/sync.py +2 -0
- polymath/memory/writer.py +9 -0
- polymath/model_policy.py +172 -0
- polymath/orchestrator/__init__.py +68 -0
- polymath/orchestrator/attempt_ledger.py +34 -0
- polymath/orchestrator/ensemble.py +229 -0
- polymath/orchestrator/fanout.py +322 -0
- polymath/orchestrator/output_policy.py +61 -0
- polymath/orchestrator/race.py +311 -0
- polymath/orchestrator/run_controller.py +91 -0
- polymath/orchestrator/speculative_review.py +120 -0
- polymath/orchestrator/state_responder.py +184 -0
- polymath/orchestrator/worker_pool.py +37 -0
- polymath/permissions.py +82 -0
- polymath/pipeline.py +700 -0
- polymath/project_config.py +229 -0
- polymath/project_runtime.py +109 -0
- polymath/router.py +127 -0
- polymath/setup_wizard.py +106 -0
- polymath/slash_commands.py +566 -0
- polymath/subagents.py +486 -0
- polymath/tools.py +333 -0
- polymath/ui_state.py +84 -0
- polymath/workspace.py +66 -0
- polymath_agent-0.4.0.dist-info/METADATA +693 -0
- polymath_agent-0.4.0.dist-info/RECORD +54 -0
- polymath_agent-0.4.0.dist-info/WHEEL +5 -0
- polymath_agent-0.4.0.dist-info/entry_points.txt +2 -0
- polymath_agent-0.4.0.dist-info/licenses/LICENSE +21 -0
- polymath_agent-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from polymath.config import PRIORITY_PROFILES, TaskType
|
|
6
|
+
from polymath.router import select_primary
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class StateResponder:
|
|
10
|
+
_MODEL_PATTERNS = (
|
|
11
|
+
"what all models are you using",
|
|
12
|
+
"what all models do you use",
|
|
13
|
+
"which all models are you using",
|
|
14
|
+
"which all models do you use",
|
|
15
|
+
"what models are you using",
|
|
16
|
+
"which models are you using",
|
|
17
|
+
"what model are you using",
|
|
18
|
+
"which model are you using",
|
|
19
|
+
"show models you are using",
|
|
20
|
+
"what models do you have",
|
|
21
|
+
"which models do you have",
|
|
22
|
+
)
|
|
23
|
+
_CAPABILITY_PATTERNS = (
|
|
24
|
+
"what can you do",
|
|
25
|
+
"what all can you do",
|
|
26
|
+
"what do you do",
|
|
27
|
+
"what all do you do",
|
|
28
|
+
"what are your capabilities",
|
|
29
|
+
"what can polymath do",
|
|
30
|
+
"help me with",
|
|
31
|
+
)
|
|
32
|
+
_COMPARISON_PATTERNS = (
|
|
33
|
+
"better than",
|
|
34
|
+
"stronger than",
|
|
35
|
+
"why not",
|
|
36
|
+
"why are you using",
|
|
37
|
+
"shouldn't you use",
|
|
38
|
+
"isn't",
|
|
39
|
+
)
|
|
40
|
+
_PROVIDER_WORDS = ("claude", "gemini", "gpt", "openai", "ollama", "lm studio", "llama", "mistral", "opus", "sonnet", "haiku")
|
|
41
|
+
|
|
42
|
+
def can_answer(self, text: str) -> bool:
|
|
43
|
+
lowered = text.lower().strip()
|
|
44
|
+
return (
|
|
45
|
+
any(pattern in lowered for pattern in self._MODEL_PATTERNS)
|
|
46
|
+
or any(pattern in lowered for pattern in self._CAPABILITY_PATTERNS)
|
|
47
|
+
or self._is_model_policy_question(lowered)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def _is_model_policy_question(self, lowered: str) -> bool:
|
|
51
|
+
return (
|
|
52
|
+
any(pattern in lowered for pattern in self._COMPARISON_PATTERNS)
|
|
53
|
+
and any(word in lowered for word in self._PROVIDER_WORDS)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def _resolve_primary(self, available, profile: str):
|
|
57
|
+
primary = select_primary(list(available), TaskType.GENERAL, profile)
|
|
58
|
+
if primary:
|
|
59
|
+
return primary
|
|
60
|
+
ordered = sorted(
|
|
61
|
+
available,
|
|
62
|
+
key=PRIORITY_PROFILES.get(profile, PRIORITY_PROFILES["quality-first"]),
|
|
63
|
+
)
|
|
64
|
+
return ordered[0]
|
|
65
|
+
|
|
66
|
+
def _model_aliases(self, model) -> list[tuple[str, int]]:
|
|
67
|
+
display = model.display_name.lower()
|
|
68
|
+
aliases: list[tuple[str, int]] = [
|
|
69
|
+
(display, 120),
|
|
70
|
+
(re.sub(r"[^a-z0-9]+", "", display), 100),
|
|
71
|
+
(model.id.lower(), 90),
|
|
72
|
+
]
|
|
73
|
+
tokens = [token for token in re.findall(r"[a-z0-9]+", display) if len(token) > 2]
|
|
74
|
+
for token in tokens:
|
|
75
|
+
aliases.append((token, 18))
|
|
76
|
+
aliases.append((model.provider.lower(), 4))
|
|
77
|
+
return aliases
|
|
78
|
+
|
|
79
|
+
def _score_model_match(self, text: str, model) -> int:
|
|
80
|
+
lowered = text.lower()
|
|
81
|
+
compact_text = re.sub(r"[^a-z0-9]+", "", lowered)
|
|
82
|
+
score = 0
|
|
83
|
+
for alias, weight in self._model_aliases(model):
|
|
84
|
+
if not alias:
|
|
85
|
+
continue
|
|
86
|
+
if alias == model.provider.lower():
|
|
87
|
+
if re.search(rf"\b{re.escape(alias)}\b", lowered):
|
|
88
|
+
score += weight
|
|
89
|
+
continue
|
|
90
|
+
if alias == re.sub(r"[^a-z0-9]+", "", alias):
|
|
91
|
+
if alias in compact_text:
|
|
92
|
+
score += weight
|
|
93
|
+
elif alias in lowered:
|
|
94
|
+
score += weight
|
|
95
|
+
elif re.search(rf"\b{re.escape(alias)}\b", lowered):
|
|
96
|
+
score += weight
|
|
97
|
+
return score
|
|
98
|
+
|
|
99
|
+
def _best_match(self, text: str, registry):
|
|
100
|
+
scored = []
|
|
101
|
+
for model in registry:
|
|
102
|
+
score = self._score_model_match(text, model)
|
|
103
|
+
if score > 0:
|
|
104
|
+
scored.append((score, model))
|
|
105
|
+
if not scored:
|
|
106
|
+
return None
|
|
107
|
+
scored.sort(key=lambda item: (-item[0], -item[1].quality, -item[1].context_window, -item[1].speed))
|
|
108
|
+
return scored[0][1]
|
|
109
|
+
|
|
110
|
+
def _split_comparison_sides(self, text: str) -> tuple[str, str] | None:
|
|
111
|
+
lowered = text.lower().strip()
|
|
112
|
+
for marker in ("better than", "stronger than", "over"):
|
|
113
|
+
if marker in lowered:
|
|
114
|
+
left, right = lowered.split(marker, 1)
|
|
115
|
+
return left.strip(), right.strip()
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
def _find_mentions(self, text: str, registry) -> list:
|
|
119
|
+
parts = self._split_comparison_sides(text)
|
|
120
|
+
if parts:
|
|
121
|
+
left = self._best_match(parts[0], registry)
|
|
122
|
+
right = self._best_match(parts[1], registry)
|
|
123
|
+
resolved = []
|
|
124
|
+
for model in (left, right):
|
|
125
|
+
if model and all(existing.id != model.id for existing in resolved):
|
|
126
|
+
resolved.append(model)
|
|
127
|
+
if len(resolved) >= 2:
|
|
128
|
+
return resolved[:2]
|
|
129
|
+
scored_matches: list[tuple[int, object]] = []
|
|
130
|
+
for model in registry:
|
|
131
|
+
score = self._score_model_match(text, model)
|
|
132
|
+
if score > 0:
|
|
133
|
+
scored_matches.append((score, model))
|
|
134
|
+
scored_matches.sort(key=lambda item: (-item[0], -item[1].quality, -item[1].context_window, -item[1].speed))
|
|
135
|
+
seen = set()
|
|
136
|
+
unique = []
|
|
137
|
+
for _, model in scored_matches:
|
|
138
|
+
if model.id not in seen:
|
|
139
|
+
unique.append(model)
|
|
140
|
+
seen.add(model.id)
|
|
141
|
+
return unique[:2]
|
|
142
|
+
|
|
143
|
+
def answer(self, text: str, registry, profile: str) -> str | None:
|
|
144
|
+
if not self.can_answer(text):
|
|
145
|
+
return None
|
|
146
|
+
available = [m for m in registry if m.available]
|
|
147
|
+
if not available:
|
|
148
|
+
return "I don't currently have any available models."
|
|
149
|
+
lowered = text.lower().strip()
|
|
150
|
+
if any(pattern in lowered for pattern in self._CAPABILITY_PATTERNS):
|
|
151
|
+
return (
|
|
152
|
+
"I can route across available models, answer direct questions, run the multi-step planning/execution/review pipeline, "
|
|
153
|
+
"use repo or detached project context, manage queued prompts, run slash commands and subagents, and work with git-backed shared `.polymath/` project metadata."
|
|
154
|
+
)
|
|
155
|
+
ordered = sorted(
|
|
156
|
+
available,
|
|
157
|
+
key=PRIORITY_PROFILES.get(profile, PRIORITY_PROFILES["quality-first"]),
|
|
158
|
+
)
|
|
159
|
+
primary = self._resolve_primary(available, profile)
|
|
160
|
+
if self._is_model_policy_question(lowered):
|
|
161
|
+
mentioned = self._find_mentions(lowered, available)
|
|
162
|
+
if len(mentioned) >= 2:
|
|
163
|
+
left, right = mentioned[0], mentioned[1]
|
|
164
|
+
left_score = (left.quality, left.context_window, left.speed)
|
|
165
|
+
right_score = (right.quality, right.context_window, right.speed)
|
|
166
|
+
if left_score == right_score:
|
|
167
|
+
verdict = f"In the current registry, {left.display_name} and {right.display_name} are effectively tied on the stored policy metrics."
|
|
168
|
+
elif left_score > right_score:
|
|
169
|
+
verdict = f"Yes. In the current registry, {left.display_name} ranks above {right.display_name} on raw policy strength."
|
|
170
|
+
else:
|
|
171
|
+
verdict = f"No. In the current registry, {right.display_name} ranks above {left.display_name} on raw policy strength."
|
|
172
|
+
return (
|
|
173
|
+
f"{verdict} Under `{profile}`, the current primary choice for a general task is {primary.display_name}. "
|
|
174
|
+
f"`quality-first` now breaks ties by quality, then context window, then speed, then cost."
|
|
175
|
+
)
|
|
176
|
+
return (
|
|
177
|
+
f"Under `{profile}`, my current primary choice for a general task is {primary.display_name}. "
|
|
178
|
+
f"`quality-first` now breaks ties by quality, then context window, then speed, then cost."
|
|
179
|
+
)
|
|
180
|
+
names = ", ".join(m.display_name for m in ordered)
|
|
181
|
+
return (
|
|
182
|
+
f"I'm currently orchestrating across {len(ordered)} available models: {names}. "
|
|
183
|
+
f"My current primary choice under the `{profile}` profile is {primary.display_name}."
|
|
184
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any, Awaitable, Callable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class WorkerPool:
|
|
10
|
+
tasks: set[asyncio.Task[Any]] = field(default_factory=set)
|
|
11
|
+
|
|
12
|
+
async def run(self, factory: Callable[[], Awaitable[Any]]) -> Any:
|
|
13
|
+
task = asyncio.create_task(factory())
|
|
14
|
+
self.tasks.add(task)
|
|
15
|
+
try:
|
|
16
|
+
return await task
|
|
17
|
+
finally:
|
|
18
|
+
self.tasks.discard(task)
|
|
19
|
+
|
|
20
|
+
async def gather(self, factories: list[Callable[[], Awaitable[Any]]]) -> list[Any]:
|
|
21
|
+
if not factories:
|
|
22
|
+
return []
|
|
23
|
+
tasks = [asyncio.create_task(factory()) for factory in factories]
|
|
24
|
+
self.tasks.update(tasks)
|
|
25
|
+
try:
|
|
26
|
+
return await asyncio.gather(*tasks, return_exceptions=True)
|
|
27
|
+
finally:
|
|
28
|
+
for task in tasks:
|
|
29
|
+
self.tasks.discard(task)
|
|
30
|
+
|
|
31
|
+
async def cancel_all(self) -> None:
|
|
32
|
+
if not self.tasks:
|
|
33
|
+
return
|
|
34
|
+
for task in list(self.tasks):
|
|
35
|
+
task.cancel()
|
|
36
|
+
await asyncio.gather(*list(self.tasks), return_exceptions=True)
|
|
37
|
+
self.tasks.clear()
|
polymath/permissions.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
from polymath.domain import PermissionRequest
|
|
6
|
+
from polymath.project_config import LocalProject, is_shell_command_allowed, is_tool_allowed
|
|
7
|
+
from polymath.tools import permission_scope
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
#: Tools whose first argument is a command line, so the shell allowlist
|
|
11
|
+
#: applies to them as well as the tool allowlist.
|
|
12
|
+
SHELL_TOOLS = ("run_shell_command",)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_permission_request(
|
|
16
|
+
tool_name: str,
|
|
17
|
+
arguments: dict[str, Any] | None = None,
|
|
18
|
+
) -> PermissionRequest:
|
|
19
|
+
"""Build a permission request straight from the tool call.
|
|
20
|
+
|
|
21
|
+
The decision is made on `tool_name` and `arguments`. `action` is derived
|
|
22
|
+
from them only so the prompt has something to show, which means what the
|
|
23
|
+
user is asked about and what is checked are the same values.
|
|
24
|
+
|
|
25
|
+
This replaces an earlier version that rendered the call to a sentence and
|
|
26
|
+
recovered the command from it with a regular expression. That recovery
|
|
27
|
+
stopped at the first quote, so a command containing one was approved on
|
|
28
|
+
the strength of a truncated fragment.
|
|
29
|
+
"""
|
|
30
|
+
arguments = dict(arguments or {})
|
|
31
|
+
shell_command = ""
|
|
32
|
+
if tool_name in SHELL_TOOLS:
|
|
33
|
+
raw = arguments.get("command", "")
|
|
34
|
+
shell_command = raw if isinstance(raw, str) else ""
|
|
35
|
+
reason = permission_scope(tool_name, arguments)
|
|
36
|
+
return PermissionRequest(
|
|
37
|
+
action=render_action(tool_name, arguments, reason),
|
|
38
|
+
tool_name=tool_name,
|
|
39
|
+
shell_command=shell_command,
|
|
40
|
+
arguments=arguments,
|
|
41
|
+
escalation_reason=reason,
|
|
42
|
+
requires_prompt=True,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def render_action(
|
|
47
|
+
tool_name: str,
|
|
48
|
+
arguments: dict[str, Any],
|
|
49
|
+
escalation_reason: str = "",
|
|
50
|
+
) -> str:
|
|
51
|
+
"""Human-readable one-liner for the permission prompt. Display only."""
|
|
52
|
+
line = f"Execute {tool_name} with {arguments}"
|
|
53
|
+
if escalation_reason:
|
|
54
|
+
line += f" [{escalation_reason}]"
|
|
55
|
+
return line
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def is_preapproved(request: PermissionRequest, local_project: LocalProject | None) -> bool:
|
|
59
|
+
# A project's allowed_tools list says "read_file is fine here". It does
|
|
60
|
+
# not say "and my ssh key too" — an escalated call is always asked about.
|
|
61
|
+
if request.escalation_reason:
|
|
62
|
+
return False
|
|
63
|
+
if not local_project or not request.tool_name:
|
|
64
|
+
return False
|
|
65
|
+
if not is_tool_allowed(request.tool_name, local_project.settings):
|
|
66
|
+
return False
|
|
67
|
+
if request.tool_name != "run_shell_command":
|
|
68
|
+
return True
|
|
69
|
+
return bool(request.shell_command) and is_shell_command_allowed(
|
|
70
|
+
request.shell_command,
|
|
71
|
+
local_project.settings,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def decide_permission(
|
|
76
|
+
request: PermissionRequest,
|
|
77
|
+
local_project: LocalProject | None,
|
|
78
|
+
prompt_user: Callable[[str], bool],
|
|
79
|
+
) -> bool:
|
|
80
|
+
if is_preapproved(request, local_project):
|
|
81
|
+
return True
|
|
82
|
+
return prompt_user(request.action)
|