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,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Legacy facade — re-exports the brain's memory module at its old path.
|
|
3
|
+
|
|
4
|
+
Implementation lives in `nexus.memory`. This package will keep
|
|
5
|
+
existing forever for backwards compatibility with old imports.
|
|
6
|
+
|
|
7
|
+
# Legacy
|
|
8
|
+
from polymath.memory.store import Chunk, MemoryStore
|
|
9
|
+
|
|
10
|
+
# Preferred (post-decoupling)
|
|
11
|
+
from nexus import Chunk, MemoryStore
|
|
12
|
+
"""
|
|
13
|
+
from nexus.memory.store import Chunk, MemoryStore, resolve_memory_root
|
|
14
|
+
|
|
15
|
+
__all__ = ["Chunk", "MemoryStore", "resolve_memory_root"]
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Embedding adapters: Gemini text-embedding-004 (primary), OpenAI text-embedding-3-small (fallback).
|
|
3
|
+
|
|
4
|
+
Reuses key resolution from polymath.config / polymath.detector. Same provider
|
|
5
|
+
ordering as the chat adapters — Gemini's free tier first, OpenAI as paid fallback.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Sequence
|
|
12
|
+
|
|
13
|
+
from polymath.adapters.base import AuthExpiredError
|
|
14
|
+
from polymath.config import get_api_key
|
|
15
|
+
from polymath.detector import _read_codex_cli_key, _read_gemini_cli_key
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
GEMINI_MODEL = "text-embedding-004"
|
|
19
|
+
GEMINI_DIM = 768
|
|
20
|
+
OPENAI_MODEL = "text-embedding-3-small"
|
|
21
|
+
OPENAI_DIM = 1536
|
|
22
|
+
|
|
23
|
+
PROVIDER_DIMS = {
|
|
24
|
+
("gemini", "text-embedding-004"): 768,
|
|
25
|
+
("openai", "text-embedding-3-small"): 1536,
|
|
26
|
+
("openai", "text-embedding-3-large"): 3072,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class EmbedResult:
|
|
32
|
+
provider: str
|
|
33
|
+
model: str
|
|
34
|
+
dim: int
|
|
35
|
+
vectors: list[list[float]]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class EmbedderUnavailable(RuntimeError):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Embedder:
|
|
43
|
+
"""Tries Gemini first, then OpenAI. Caller picks an instance and stays on it
|
|
44
|
+
for the lifetime of a memory write — switching mid-batch would mix dims."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, cfg: dict, prefer: str = "gemini") -> None:
|
|
47
|
+
self.cfg = cfg
|
|
48
|
+
self.prefer = prefer
|
|
49
|
+
self._gemini_client = None
|
|
50
|
+
self._openai_client = None
|
|
51
|
+
self._gemini_types = None
|
|
52
|
+
self._active: tuple[str, str, int] | None = None
|
|
53
|
+
|
|
54
|
+
# ── Public API ────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
async def embed(self, texts: Sequence[str]) -> EmbedResult:
|
|
57
|
+
if not texts:
|
|
58
|
+
return EmbedResult(provider="", model="", dim=0, vectors=[])
|
|
59
|
+
|
|
60
|
+
order = ["gemini", "openai"] if self.prefer == "gemini" else ["openai", "gemini"]
|
|
61
|
+
failures: list[str] = []
|
|
62
|
+
for provider in order:
|
|
63
|
+
try:
|
|
64
|
+
if provider == "gemini":
|
|
65
|
+
return await self._embed_gemini(list(texts))
|
|
66
|
+
return await self._embed_openai(list(texts))
|
|
67
|
+
except (EmbedderUnavailable, AuthExpiredError, Exception) as e:
|
|
68
|
+
failures.append(f"{provider}: {e}")
|
|
69
|
+
guidance = (
|
|
70
|
+
"no embedding provider available.\n " + "\n ".join(failures) +
|
|
71
|
+
"\nfix: set GOOGLE_API_KEY (free at https://aistudio.google.com/apikey) "
|
|
72
|
+
"or OPENAI_API_KEY (codex OAuth tokens are chat-only and cannot embed)."
|
|
73
|
+
)
|
|
74
|
+
raise EmbedderUnavailable(guidance)
|
|
75
|
+
|
|
76
|
+
def active_model(self) -> tuple[str, str, int] | None:
|
|
77
|
+
return self._active
|
|
78
|
+
|
|
79
|
+
# ── Gemini ────────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
def _ensure_gemini(self) -> None:
|
|
82
|
+
if self._gemini_client is not None:
|
|
83
|
+
return
|
|
84
|
+
try:
|
|
85
|
+
from google import genai
|
|
86
|
+
from google.genai import types as genai_types
|
|
87
|
+
except ImportError as e:
|
|
88
|
+
raise EmbedderUnavailable(f"google-genai not installed: {e}")
|
|
89
|
+
|
|
90
|
+
key = get_api_key("gemini", self.cfg) or _read_gemini_cli_key() or ""
|
|
91
|
+
if not key:
|
|
92
|
+
raise EmbedderUnavailable("no gemini api key")
|
|
93
|
+
|
|
94
|
+
if key.startswith("ya29."):
|
|
95
|
+
self._gemini_client = genai.Client(
|
|
96
|
+
http_options={"headers": {"Authorization": f"Bearer {key}"}}
|
|
97
|
+
)
|
|
98
|
+
else:
|
|
99
|
+
self._gemini_client = genai.Client(api_key=key)
|
|
100
|
+
self._gemini_types = genai_types
|
|
101
|
+
|
|
102
|
+
async def _embed_gemini(self, texts: list[str]) -> EmbedResult:
|
|
103
|
+
self._ensure_gemini()
|
|
104
|
+
client = self._gemini_client
|
|
105
|
+
all_vectors: list[list[float]] = []
|
|
106
|
+
for batch in _batches(texts, 100):
|
|
107
|
+
try:
|
|
108
|
+
resp = await client.aio.models.embed_content(
|
|
109
|
+
model=GEMINI_MODEL,
|
|
110
|
+
contents=batch,
|
|
111
|
+
)
|
|
112
|
+
except Exception as e:
|
|
113
|
+
msg = str(e).lower()
|
|
114
|
+
if "401" in msg or "unauthenticated" in msg or "invalid_grant" in msg:
|
|
115
|
+
raise AuthExpiredError("gemini", "gemini auth login") from e
|
|
116
|
+
raise
|
|
117
|
+
for embedding in resp.embeddings:
|
|
118
|
+
values = list(getattr(embedding, "values", []) or [])
|
|
119
|
+
all_vectors.append(values)
|
|
120
|
+
self._active = ("gemini", GEMINI_MODEL, GEMINI_DIM)
|
|
121
|
+
return EmbedResult(provider="gemini", model=GEMINI_MODEL, dim=GEMINI_DIM, vectors=all_vectors)
|
|
122
|
+
|
|
123
|
+
# ── OpenAI ────────────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
def _ensure_openai(self) -> None:
|
|
126
|
+
if self._openai_client is not None:
|
|
127
|
+
return
|
|
128
|
+
try:
|
|
129
|
+
from openai import AsyncOpenAI
|
|
130
|
+
except ImportError as e:
|
|
131
|
+
raise EmbedderUnavailable(f"openai not installed: {e}")
|
|
132
|
+
|
|
133
|
+
key = get_api_key("openai", self.cfg) or _read_codex_cli_key() or ""
|
|
134
|
+
if not key:
|
|
135
|
+
raise EmbedderUnavailable("no openai api key")
|
|
136
|
+
if key.startswith("chatgpt_oauth:"):
|
|
137
|
+
token = key[len("chatgpt_oauth:"):]
|
|
138
|
+
self._openai_client = AsyncOpenAI(
|
|
139
|
+
api_key="unused",
|
|
140
|
+
default_headers={"Authorization": f"Bearer {token}"},
|
|
141
|
+
)
|
|
142
|
+
else:
|
|
143
|
+
self._openai_client = AsyncOpenAI(api_key=key)
|
|
144
|
+
|
|
145
|
+
async def _embed_openai(self, texts: list[str]) -> EmbedResult:
|
|
146
|
+
self._ensure_openai()
|
|
147
|
+
client = self._openai_client
|
|
148
|
+
all_vectors: list[list[float]] = []
|
|
149
|
+
for batch in _batches(texts, 100):
|
|
150
|
+
try:
|
|
151
|
+
resp = await client.embeddings.create(
|
|
152
|
+
model=OPENAI_MODEL,
|
|
153
|
+
input=batch,
|
|
154
|
+
)
|
|
155
|
+
except Exception as e:
|
|
156
|
+
msg = str(e).lower()
|
|
157
|
+
if "401" in msg or "authentication" in msg:
|
|
158
|
+
raise AuthExpiredError("openai", "codex login") from e
|
|
159
|
+
raise
|
|
160
|
+
for item in resp.data:
|
|
161
|
+
all_vectors.append(list(item.embedding))
|
|
162
|
+
self._active = ("openai", OPENAI_MODEL, OPENAI_DIM)
|
|
163
|
+
return EmbedResult(provider="openai", model=OPENAI_MODEL, dim=OPENAI_DIM, vectors=all_vectors)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _batches(items: list, size: int):
|
|
167
|
+
for i in range(0, len(items), size):
|
|
168
|
+
yield items[i:i + size]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def cosine(a: list[float], b: list[float]) -> float:
|
|
172
|
+
if not a or not b or len(a) != len(b):
|
|
173
|
+
return 0.0
|
|
174
|
+
dot = sum(x * y for x, y in zip(a, b))
|
|
175
|
+
na = sum(x * x for x in a) ** 0.5
|
|
176
|
+
nb = sum(y * y for y in b) ** 0.5
|
|
177
|
+
if na == 0 or nb == 0:
|
|
178
|
+
return 0.0
|
|
179
|
+
return dot / (na * nb)
|
polymath/memory/store.py
ADDED
polymath/memory/sync.py
ADDED
polymath/model_policy.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from polymath.adapters import ClaudeAdapter, GeminiAdapter, OllamaAdapter, OpenAIAdapter
|
|
6
|
+
from polymath.adapters.base import BaseAdapter
|
|
7
|
+
from polymath.compressor import build_context
|
|
8
|
+
from polymath.config import TaskType, get_api_key
|
|
9
|
+
from polymath.context_manager import build_context_injection
|
|
10
|
+
from polymath.detector import _read_claude_cli_key, _read_codex_cli_key, _read_gemini_cli_key
|
|
11
|
+
from polymath.router import select_primary, select_reviewer, select_summarizer
|
|
12
|
+
from polymath.workspace import get_workspace_context
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
CONTEXT_HEADROOM_TOKENS = 2048
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def adapter_for(provider: str, cfg: dict) -> BaseAdapter | None:
|
|
19
|
+
key = ""
|
|
20
|
+
try:
|
|
21
|
+
if provider in ("claude", "gemini", "openai"):
|
|
22
|
+
key = get_api_key(provider, cfg) or ""
|
|
23
|
+
if not key:
|
|
24
|
+
readers = {
|
|
25
|
+
"claude": _read_claude_cli_key,
|
|
26
|
+
"gemini": _read_gemini_cli_key,
|
|
27
|
+
"openai": _read_codex_cli_key,
|
|
28
|
+
}
|
|
29
|
+
key = readers[provider]() or ""
|
|
30
|
+
if provider == "claude":
|
|
31
|
+
return ClaudeAdapter(api_key=key)
|
|
32
|
+
if provider == "gemini":
|
|
33
|
+
return GeminiAdapter(api_key=key)
|
|
34
|
+
if provider == "openai":
|
|
35
|
+
return OpenAIAdapter(api_key=key)
|
|
36
|
+
if provider == "ollama":
|
|
37
|
+
return OllamaAdapter(base_url=cfg.get("ollama_url", "http://localhost:11434"))
|
|
38
|
+
if provider == "lmstudio":
|
|
39
|
+
url = cfg.get("lmstudio_url", "http://localhost:1234").rstrip("/") + "/v1"
|
|
40
|
+
return OpenAIAdapter(api_key="", base_url=url)
|
|
41
|
+
except Exception:
|
|
42
|
+
return None
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def estimate_prompt_tokens(
|
|
47
|
+
session_id: str,
|
|
48
|
+
user_input: str,
|
|
49
|
+
active_project: str,
|
|
50
|
+
task_type: TaskType,
|
|
51
|
+
adapter: BaseAdapter,
|
|
52
|
+
summarizer_adapter: BaseAdapter | None,
|
|
53
|
+
summarizer_model_id: str,
|
|
54
|
+
is_ask: bool,
|
|
55
|
+
model_id: str = "",
|
|
56
|
+
) -> int:
|
|
57
|
+
context = await build_context(
|
|
58
|
+
session_id,
|
|
59
|
+
summarizer=summarizer_adapter,
|
|
60
|
+
summarizer_model=summarizer_model_id,
|
|
61
|
+
)
|
|
62
|
+
prompt_parts = [f"{m.role}: {m.content}" for m in context]
|
|
63
|
+
prompt_parts.append(f"user: {user_input}")
|
|
64
|
+
project_ctx = build_context_injection(active_project, task_type) if active_project else ""
|
|
65
|
+
if is_ask:
|
|
66
|
+
system = "Answer concisely and directly."
|
|
67
|
+
if project_ctx:
|
|
68
|
+
system += f"\n\n{project_ctx}"
|
|
69
|
+
else:
|
|
70
|
+
system = (
|
|
71
|
+
"You are an autonomous agent capable of using tools to interact with your environment. "
|
|
72
|
+
"When a task requires information you don't have, use a tool to get it. "
|
|
73
|
+
"Always reason out loud before taking an action. "
|
|
74
|
+
"If you have all the information needed, provide a final answer. "
|
|
75
|
+
"Be precise and thorough."
|
|
76
|
+
)
|
|
77
|
+
if project_ctx:
|
|
78
|
+
system += f"\n\n{project_ctx}"
|
|
79
|
+
system += f"\n\n{get_workspace_context()}"
|
|
80
|
+
return await adapter.count_tokens_async(
|
|
81
|
+
system + "\n\n" + "\n".join(prompt_parts), model_id,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class ModelResolution:
|
|
87
|
+
primary_info: object | None
|
|
88
|
+
primary_adapter: BaseAdapter | None
|
|
89
|
+
reviewer_info: object | None
|
|
90
|
+
reviewer_adapter: BaseAdapter | None
|
|
91
|
+
summarizer_info: object | None
|
|
92
|
+
summarizer_adapter: BaseAdapter | None
|
|
93
|
+
estimated_prompt_tokens: int = 0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def resolve_models(
|
|
97
|
+
registry,
|
|
98
|
+
cfg: dict,
|
|
99
|
+
session_id: str,
|
|
100
|
+
user_input: str,
|
|
101
|
+
active_project: str,
|
|
102
|
+
task_type: TaskType,
|
|
103
|
+
profile: str,
|
|
104
|
+
force_provider: str = "",
|
|
105
|
+
cheap: bool = False,
|
|
106
|
+
fast: bool = False,
|
|
107
|
+
is_ask: bool = False,
|
|
108
|
+
) -> ModelResolution:
|
|
109
|
+
summarizer_info = select_summarizer(registry)
|
|
110
|
+
summarizer_adapter = adapter_for(summarizer_info.provider, cfg) if summarizer_info else None
|
|
111
|
+
|
|
112
|
+
excluded: set[str] = set()
|
|
113
|
+
primary_info = None
|
|
114
|
+
primary_adapter = None
|
|
115
|
+
estimated_prompt_tokens = 0
|
|
116
|
+
while True:
|
|
117
|
+
candidates = [m for m in registry if m.id not in excluded]
|
|
118
|
+
primary_info = select_primary(
|
|
119
|
+
candidates,
|
|
120
|
+
task_type,
|
|
121
|
+
profile,
|
|
122
|
+
force_provider=force_provider,
|
|
123
|
+
force_cheap=cheap,
|
|
124
|
+
force_fast=fast,
|
|
125
|
+
)
|
|
126
|
+
if not primary_info:
|
|
127
|
+
break
|
|
128
|
+
primary_adapter = adapter_for(primary_info.provider, cfg)
|
|
129
|
+
if not primary_adapter:
|
|
130
|
+
excluded.add(primary_info.id)
|
|
131
|
+
continue
|
|
132
|
+
estimated_prompt_tokens = await estimate_prompt_tokens(
|
|
133
|
+
model_id=primary_info.id,
|
|
134
|
+
session_id=session_id,
|
|
135
|
+
user_input=user_input,
|
|
136
|
+
active_project=active_project,
|
|
137
|
+
task_type=task_type,
|
|
138
|
+
adapter=primary_adapter,
|
|
139
|
+
summarizer_adapter=summarizer_adapter,
|
|
140
|
+
summarizer_model_id=summarizer_info.id if summarizer_info else "",
|
|
141
|
+
is_ask=is_ask,
|
|
142
|
+
)
|
|
143
|
+
if primary_info.context_window - estimated_prompt_tokens > CONTEXT_HEADROOM_TOKENS:
|
|
144
|
+
break
|
|
145
|
+
excluded.add(primary_info.id)
|
|
146
|
+
reviewer_info = select_reviewer(registry, primary_info, profile) if primary_info else None
|
|
147
|
+
reviewer_adapter = adapter_for(reviewer_info.provider, cfg) if reviewer_info else None
|
|
148
|
+
return ModelResolution(
|
|
149
|
+
primary_info=primary_info,
|
|
150
|
+
primary_adapter=primary_adapter,
|
|
151
|
+
reviewer_info=reviewer_info,
|
|
152
|
+
reviewer_adapter=reviewer_adapter,
|
|
153
|
+
summarizer_info=summarizer_info,
|
|
154
|
+
summarizer_adapter=summarizer_adapter,
|
|
155
|
+
estimated_prompt_tokens=estimated_prompt_tokens,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def try_fallback_adapter(registry, failed_model, profile: str, cfg: dict, tried_model_ids: set[str] | None = None):
|
|
160
|
+
tried = set(tried_model_ids or set())
|
|
161
|
+
tried.add(failed_model.id)
|
|
162
|
+
available = [m for m in registry if m.available and m.id not in tried]
|
|
163
|
+
while available:
|
|
164
|
+
candidate = select_primary(available, TaskType.GENERAL, profile)
|
|
165
|
+
if not candidate or candidate.id in tried:
|
|
166
|
+
break
|
|
167
|
+
tried.add(candidate.id)
|
|
168
|
+
adapter = adapter_for(candidate.provider, cfg)
|
|
169
|
+
if adapter:
|
|
170
|
+
return candidate, adapter
|
|
171
|
+
available = [m for m in available if m.id not in tried]
|
|
172
|
+
return None
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from polymath.orchestrator.attempt_ledger import AttemptLedger
|
|
2
|
+
from polymath.orchestrator.ensemble import (
|
|
3
|
+
EnsembleResult,
|
|
4
|
+
EnsembleWorker,
|
|
5
|
+
WorkerResult,
|
|
6
|
+
run_ensemble,
|
|
7
|
+
select_ensemble_workers,
|
|
8
|
+
)
|
|
9
|
+
from polymath.orchestrator.fanout import (
|
|
10
|
+
FanoutResult,
|
|
11
|
+
Subtask,
|
|
12
|
+
SubtaskResult,
|
|
13
|
+
run_fanout,
|
|
14
|
+
select_splitter_and_joiner,
|
|
15
|
+
)
|
|
16
|
+
from polymath.orchestrator.speculative_review import SpeculativeReviewer
|
|
17
|
+
from polymath.orchestrator.output_policy import FailureDecision, OutputPolicy
|
|
18
|
+
from polymath.orchestrator.race import (
|
|
19
|
+
AllCandidatesFailed,
|
|
20
|
+
RaceCandidate,
|
|
21
|
+
RaceOutcome,
|
|
22
|
+
StreamCandidate,
|
|
23
|
+
StreamOutcome,
|
|
24
|
+
gather_all,
|
|
25
|
+
race_first_success,
|
|
26
|
+
race_streams,
|
|
27
|
+
with_failover,
|
|
28
|
+
)
|
|
29
|
+
from polymath.orchestrator.run_controller import (
|
|
30
|
+
ExecutionStrategy,
|
|
31
|
+
RunController,
|
|
32
|
+
RunDecision,
|
|
33
|
+
RunRequest,
|
|
34
|
+
)
|
|
35
|
+
from polymath.orchestrator.state_responder import StateResponder
|
|
36
|
+
from polymath.orchestrator.worker_pool import WorkerPool
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"AllCandidatesFailed",
|
|
40
|
+
"AttemptLedger",
|
|
41
|
+
"EnsembleResult",
|
|
42
|
+
"EnsembleWorker",
|
|
43
|
+
"ExecutionStrategy",
|
|
44
|
+
"FailureDecision",
|
|
45
|
+
"FanoutResult",
|
|
46
|
+
"Subtask",
|
|
47
|
+
"SubtaskResult",
|
|
48
|
+
"OutputPolicy",
|
|
49
|
+
"RaceCandidate",
|
|
50
|
+
"RaceOutcome",
|
|
51
|
+
"StreamCandidate",
|
|
52
|
+
"StreamOutcome",
|
|
53
|
+
"RunController",
|
|
54
|
+
"RunDecision",
|
|
55
|
+
"RunRequest",
|
|
56
|
+
"SpeculativeReviewer",
|
|
57
|
+
"StateResponder",
|
|
58
|
+
"WorkerPool",
|
|
59
|
+
"WorkerResult",
|
|
60
|
+
"gather_all",
|
|
61
|
+
"race_first_success",
|
|
62
|
+
"race_streams",
|
|
63
|
+
"run_ensemble",
|
|
64
|
+
"run_fanout",
|
|
65
|
+
"select_ensemble_workers",
|
|
66
|
+
"select_splitter_and_joiner",
|
|
67
|
+
"with_failover",
|
|
68
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class AttemptFailure:
|
|
8
|
+
model_id: str
|
|
9
|
+
reason: str
|
|
10
|
+
retryable: bool = True
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class AttemptLedger:
|
|
15
|
+
attempted_by_phase: dict[str, set[str]] = field(default_factory=dict)
|
|
16
|
+
failures: dict[str, list[AttemptFailure]] = field(default_factory=dict)
|
|
17
|
+
|
|
18
|
+
def note_attempt(self, phase: str, model_id: str) -> None:
|
|
19
|
+
self.attempted_by_phase.setdefault(phase, set()).add(model_id)
|
|
20
|
+
|
|
21
|
+
def note_failure(self, phase: str, model_id: str, reason: str, *, retryable: bool = True) -> None:
|
|
22
|
+
self.failures.setdefault(phase, []).append(
|
|
23
|
+
AttemptFailure(model_id=model_id, reason=reason, retryable=retryable)
|
|
24
|
+
)
|
|
25
|
+
self.note_attempt(phase, model_id)
|
|
26
|
+
|
|
27
|
+
def attempted(self, phase: str) -> set[str]:
|
|
28
|
+
return set(self.attempted_by_phase.get(phase, set()))
|
|
29
|
+
|
|
30
|
+
def has_attempted(self, phase: str, model_id: str) -> bool:
|
|
31
|
+
return model_id in self.attempted_by_phase.get(phase, set())
|
|
32
|
+
|
|
33
|
+
def failures_for_phase(self, phase: str) -> list[AttemptFailure]:
|
|
34
|
+
return list(self.failures.get(phase, []))
|