eca-pp 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.
- eca_pp/__init__.py +3 -0
- eca_pp/_harness_claude.py +196 -0
- eca_pp/_harness_deepseek.py +552 -0
- eca_pp/_harness_openai.py +390 -0
- eca_pp/agent.py +116 -0
- eca_pp/core/__init__.py +2 -0
- eca_pp/core/atomic_io.py +45 -0
- eca_pp/core/colspec.py +62 -0
- eca_pp/core/columns.py +49 -0
- eca_pp/core/result.py +71 -0
- eca_pp/core/run_outputs.py +42 -0
- eca_pp/core/values.py +28 -0
- eca_pp/harness.py +241 -0
- eca_pp/identify_columns/__init__.py +1 -0
- eca_pp/identify_columns/__main__.py +3 -0
- eca_pp/identify_columns/cli.py +846 -0
- eca_pp/identify_columns/obsprofile.py +212 -0
- eca_pp/identify_columns/policies.py +256 -0
- eca_pp/probe/__init__.py +1 -0
- eca_pp/probe/__main__.py +3 -0
- eca_pp/probe/cli.py +356 -0
- eca_pp/standardize/__init__.py +1 -0
- eca_pp/standardize/__main__.py +3 -0
- eca_pp/standardize/build.py +65 -0
- eca_pp/standardize/cli.py +338 -0
- eca_pp/standardize/countsloc.py +231 -0
- eca_pp/standardize/harmonize.py +107 -0
- eca_pp/standardize/qc.py +131 -0
- eca_pp/standardize/species.py +157 -0
- eca_pp-0.4.0.dist-info/METADATA +281 -0
- eca_pp-0.4.0.dist-info/RECORD +34 -0
- eca_pp-0.4.0.dist-info/WHEEL +5 -0
- eca_pp-0.4.0.dist-info/entry_points.txt +4 -0
- eca_pp-0.4.0.dist-info/top_level.txt +1 -0
eca_pp/__init__.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""``HARNESS=claude`` backend for :mod:`eca_pp.harness`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
|
|
9
|
+
from .harness import (
|
|
10
|
+
AgentIncompleteError,
|
|
11
|
+
AgentRunResult,
|
|
12
|
+
AgentTimeout,
|
|
13
|
+
AgentUnavailable,
|
|
14
|
+
ToolSpec,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
MIN_CLAUDE_AGENT_SDK = (0, 2, 152)
|
|
18
|
+
CLI_ENV = "ECA_PP_CLAUDE_CLI"
|
|
19
|
+
_BUILTIN = {"read": ["Read"], "glob": ["Glob"], "grep": ["Grep"]}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _version_tuple(value: str) -> tuple[int, ...]:
|
|
23
|
+
result = []
|
|
24
|
+
for part in value.split("."):
|
|
25
|
+
digits = "".join(ch for ch in part if ch.isdigit())
|
|
26
|
+
if not digits:
|
|
27
|
+
break
|
|
28
|
+
result.append(int(digits))
|
|
29
|
+
return tuple(result)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def check_available() -> None:
|
|
33
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
34
|
+
|
|
35
|
+
if not os.environ.get("ANTHROPIC_API_KEY") and not os.path.isfile(
|
|
36
|
+
os.path.expanduser("~/.claude/.credentials.json")
|
|
37
|
+
):
|
|
38
|
+
raise AgentUnavailable("HARNESS=claude has no API key or Claude CLI credentials")
|
|
39
|
+
try:
|
|
40
|
+
installed = version("claude-agent-sdk")
|
|
41
|
+
except PackageNotFoundError:
|
|
42
|
+
raise AgentUnavailable(
|
|
43
|
+
"HARNESS=claude needs claude-agent-sdk>=0.2.152"
|
|
44
|
+
) from None
|
|
45
|
+
if _version_tuple(installed) < MIN_CLAUDE_AGENT_SDK:
|
|
46
|
+
floor = ".".join(map(str, MIN_CLAUDE_AGENT_SDK))
|
|
47
|
+
raise AgentUnavailable(
|
|
48
|
+
f"claude-agent-sdk {installed} is too old; install >={floor}"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _cli_path() -> str | None:
|
|
53
|
+
return os.environ.get(CLI_ENV) or shutil.which("claude")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def _bounded(stream, deadline, label: str, wall_seconds: float | None):
|
|
57
|
+
iterator = stream.__aiter__()
|
|
58
|
+
try:
|
|
59
|
+
while True:
|
|
60
|
+
timeout = None if deadline is None else max(
|
|
61
|
+
0.0, deadline - asyncio.get_running_loop().time()
|
|
62
|
+
)
|
|
63
|
+
try:
|
|
64
|
+
yield await asyncio.wait_for(iterator.__anext__(), timeout=timeout)
|
|
65
|
+
except StopAsyncIteration:
|
|
66
|
+
return
|
|
67
|
+
except asyncio.TimeoutError:
|
|
68
|
+
raise AgentTimeout(
|
|
69
|
+
f"[{label}] agent run exceeded {wall_seconds / 60:g} min "
|
|
70
|
+
"(AGENT_WALL_MIN)"
|
|
71
|
+
) from None
|
|
72
|
+
finally:
|
|
73
|
+
await iterator.aclose()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def run_agent(
|
|
77
|
+
*,
|
|
78
|
+
tools: list[ToolSpec],
|
|
79
|
+
submit_tool: str,
|
|
80
|
+
prompt: str,
|
|
81
|
+
system_prompt: str | None,
|
|
82
|
+
cwd: str,
|
|
83
|
+
model: str | None,
|
|
84
|
+
effort: str | None,
|
|
85
|
+
max_turns: int,
|
|
86
|
+
allowed_builtin: tuple[str, ...],
|
|
87
|
+
label: str,
|
|
88
|
+
max_buffer_size: int | None,
|
|
89
|
+
wall_seconds: float | None = None,
|
|
90
|
+
) -> AgentRunResult:
|
|
91
|
+
check_available()
|
|
92
|
+
from claude_agent_sdk import (
|
|
93
|
+
AssistantMessage,
|
|
94
|
+
ClaudeAgentOptions,
|
|
95
|
+
ResultMessage,
|
|
96
|
+
ToolResultBlock,
|
|
97
|
+
ToolUseBlock,
|
|
98
|
+
UserMessage,
|
|
99
|
+
create_sdk_mcp_server,
|
|
100
|
+
query,
|
|
101
|
+
tool,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
os.makedirs(cwd, exist_ok=True)
|
|
105
|
+
server_name = "eca_pp_tools"
|
|
106
|
+
submitted: dict = {}
|
|
107
|
+
|
|
108
|
+
def wrap(spec: ToolSpec):
|
|
109
|
+
is_submit = spec.name == submit_tool
|
|
110
|
+
|
|
111
|
+
@tool(spec.name, spec.description, spec.input_schema)
|
|
112
|
+
async def handler(args):
|
|
113
|
+
result = await spec.handler(args)
|
|
114
|
+
if is_submit and not result.get("is_error"):
|
|
115
|
+
submitted["value"] = result.get("_submitted", args)
|
|
116
|
+
return {key: value for key, value in result.items() if key != "_submitted"}
|
|
117
|
+
|
|
118
|
+
return handler
|
|
119
|
+
|
|
120
|
+
server = create_sdk_mcp_server(
|
|
121
|
+
name=server_name, version="1.0.0", tools=[wrap(spec) for spec in tools]
|
|
122
|
+
)
|
|
123
|
+
allowed_tools = [
|
|
124
|
+
name for builtin in allowed_builtin for name in _BUILTIN.get(builtin, [])
|
|
125
|
+
] + [f"mcp__{server_name}__{spec.name}" for spec in tools]
|
|
126
|
+
options = ClaudeAgentOptions(
|
|
127
|
+
mcp_servers={server_name: server},
|
|
128
|
+
allowed_tools=allowed_tools,
|
|
129
|
+
disallowed_tools=[
|
|
130
|
+
"Bash", "Write", "Edit", "MultiEdit", "NotebookEdit", "WebFetch",
|
|
131
|
+
"WebSearch", "Agent", "Task",
|
|
132
|
+
],
|
|
133
|
+
permission_mode="bypassPermissions",
|
|
134
|
+
cwd=cwd,
|
|
135
|
+
max_turns=max_turns,
|
|
136
|
+
system_prompt=system_prompt,
|
|
137
|
+
model=model,
|
|
138
|
+
effort=effort,
|
|
139
|
+
cli_path=_cli_path(),
|
|
140
|
+
setting_sources=[],
|
|
141
|
+
strict_mcp_config=True,
|
|
142
|
+
max_buffer_size=max_buffer_size or 32 * 1024 * 1024,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
transcript = None
|
|
146
|
+
tools_used: list[dict] = []
|
|
147
|
+
usage = {
|
|
148
|
+
"backend": "claude", "model": model, "cost_usd": None,
|
|
149
|
+
"input_tokens": None, "output_tokens": None,
|
|
150
|
+
"cache_creation_tokens": None, "cache_read_tokens": None,
|
|
151
|
+
"num_turns": None,
|
|
152
|
+
}
|
|
153
|
+
pending: dict[str, str] = {}
|
|
154
|
+
deadline = None if wall_seconds is None else \
|
|
155
|
+
asyncio.get_running_loop().time() + wall_seconds
|
|
156
|
+
async for message in _bounded(query(prompt=prompt, options=options), deadline, label, wall_seconds):
|
|
157
|
+
if isinstance(message, AssistantMessage):
|
|
158
|
+
if getattr(message, "model", None):
|
|
159
|
+
usage["model"] = message.model
|
|
160
|
+
for block in message.content:
|
|
161
|
+
if isinstance(block, ToolUseBlock):
|
|
162
|
+
pending[block.id] = block.name
|
|
163
|
+
target = str(next(iter((block.input or {}).values()), ""))[:200]
|
|
164
|
+
tools_used.append({"tool": block.name, "target": target})
|
|
165
|
+
print(f"== [{label}] agent: {block.name}({target[:80]})", flush=True)
|
|
166
|
+
elif isinstance(message, UserMessage) and isinstance(message.content, list):
|
|
167
|
+
for block in message.content:
|
|
168
|
+
if isinstance(block, ToolResultBlock) and block.is_error:
|
|
169
|
+
detail = block.content if isinstance(block.content, str) else str(block.content)
|
|
170
|
+
print(
|
|
171
|
+
f"== [{label}] tool error in {pending.get(block.tool_use_id, '?')}: "
|
|
172
|
+
f"{detail[:200]!r}", flush=True,
|
|
173
|
+
)
|
|
174
|
+
elif isinstance(message, ResultMessage):
|
|
175
|
+
transcript = message.result
|
|
176
|
+
if message.is_error or message.subtype != "success":
|
|
177
|
+
raise RuntimeError(
|
|
178
|
+
f"[{label}] Claude run ended with {message.subtype}: {message.result}"
|
|
179
|
+
)
|
|
180
|
+
raw = message.usage or {}
|
|
181
|
+
get = raw.get if isinstance(raw, dict) else lambda key, default=None: getattr(raw, key, default)
|
|
182
|
+
usage.update({
|
|
183
|
+
"cost_usd": message.total_cost_usd,
|
|
184
|
+
"input_tokens": get("input_tokens"),
|
|
185
|
+
"output_tokens": get("output_tokens"),
|
|
186
|
+
"cache_creation_tokens": get("cache_creation_input_tokens"),
|
|
187
|
+
"cache_read_tokens": get("cache_read_input_tokens"),
|
|
188
|
+
"num_turns": getattr(message, "num_turns", None),
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
if "value" not in submitted:
|
|
192
|
+
raise AgentIncompleteError(
|
|
193
|
+
f"[{label}] agent finished without a successful {submit_tool} call. "
|
|
194
|
+
f"Final reply:\n{transcript}"
|
|
195
|
+
)
|
|
196
|
+
return AgentRunResult(submitted["value"], transcript, tools_used, usage)
|