codexapi 0.5.16__tar.gz → 0.6.0__tar.gz
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.
- {codexapi-0.5.16/src/codexapi.egg-info → codexapi-0.6.0}/PKG-INFO +15 -1
- {codexapi-0.5.16 → codexapi-0.6.0}/README.md +14 -0
- {codexapi-0.5.16 → codexapi-0.6.0}/pyproject.toml +1 -1
- {codexapi-0.5.16 → codexapi-0.6.0}/src/codexapi/__init__.py +7 -1
- {codexapi-0.5.16 → codexapi-0.6.0}/src/codexapi/cli.py +7 -28
- codexapi-0.6.0/src/codexapi/pushover.py +179 -0
- codexapi-0.6.0/src/codexapi/ralph.py +374 -0
- codexapi-0.6.0/src/codexapi/science.py +301 -0
- {codexapi-0.5.16 → codexapi-0.6.0}/src/codexapi/task.py +30 -0
- {codexapi-0.5.16 → codexapi-0.6.0/src/codexapi.egg-info}/PKG-INFO +15 -1
- {codexapi-0.5.16 → codexapi-0.6.0}/src/codexapi.egg-info/SOURCES.txt +2 -0
- codexapi-0.5.16/src/codexapi/ralph.py +0 -335
- {codexapi-0.5.16 → codexapi-0.6.0}/LICENSE +0 -0
- {codexapi-0.5.16 → codexapi-0.6.0}/setup.cfg +0 -0
- {codexapi-0.5.16 → codexapi-0.6.0}/src/codexapi/__main__.py +0 -0
- {codexapi-0.5.16 → codexapi-0.6.0}/src/codexapi/agent.py +0 -0
- {codexapi-0.5.16 → codexapi-0.6.0}/src/codexapi/foreach.py +0 -0
- {codexapi-0.5.16 → codexapi-0.6.0}/src/codexapi/gh_integration.py +0 -0
- {codexapi-0.5.16 → codexapi-0.6.0}/src/codexapi/taskfile.py +0 -0
- {codexapi-0.5.16 → codexapi-0.6.0}/src/codexapi.egg-info/dependency_links.txt +0 -0
- {codexapi-0.5.16 → codexapi-0.6.0}/src/codexapi.egg-info/entry_points.txt +0 -0
- {codexapi-0.5.16 → codexapi-0.6.0}/src/codexapi.egg-info/requires.txt +0 -0
- {codexapi-0.5.16 → codexapi-0.6.0}/src/codexapi.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: codexapi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Minimal Python API for running the Codex CLI.
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: codex,agent,cli,openai
|
|
@@ -94,6 +94,12 @@ Take tasks from a GitHub Project (requires `gh-task`):
|
|
|
94
94
|
```bash
|
|
95
95
|
codexapi task -p owner/projects/3 -n "Your Name" -s Ready task_a.yaml task_b.yaml
|
|
96
96
|
```
|
|
97
|
+
Reset owned tasks on a GitHub Project back to Ready:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
codexapi reset -p owner/projects/3
|
|
101
|
+
codexapi reset -p owner/projects/3 -d # also removes the Progress section
|
|
102
|
+
```
|
|
97
103
|
|
|
98
104
|
Task labels are derived from task filenames (basename without extension). The
|
|
99
105
|
issue title/body become `{{item}}` after removing any existing `## Progress`
|
|
@@ -134,12 +140,20 @@ codexapi ralph --cancel --cwd /path/to/project
|
|
|
134
140
|
|
|
135
141
|
Science mode wraps a short task in a science prompt and runs it through the
|
|
136
142
|
Ralph loop. It defaults to `--yolo` and expects progress notes in `SCIENCE.md`.
|
|
143
|
+
Each iteration appends the agent output to `LOGBOOK.md` and the runner extracts
|
|
144
|
+
any improved figures of merit for optional notifications.
|
|
137
145
|
|
|
138
146
|
```bash
|
|
139
147
|
codexapi science "hyper-optimize the kernel cycles"
|
|
140
148
|
codexapi science --no-yolo "hyper-optimize the kernel cycles" --max-iterations 3
|
|
141
149
|
```
|
|
142
150
|
|
|
151
|
+
Optional Pushover notifications: create `~/.pushover` with two non-empty lines.
|
|
152
|
+
Line 1 is your user or group key, line 2 is the app API token. When this file
|
|
153
|
+
exists, Science will send a notification whenever it detects a new best result,
|
|
154
|
+
including the metric values and percent improvement. Task runs will also send a
|
|
155
|
+
✅/❌ notification with the task summary.
|
|
156
|
+
|
|
143
157
|
Run a task file across a list file:
|
|
144
158
|
|
|
145
159
|
```bash
|
|
@@ -79,6 +79,12 @@ Take tasks from a GitHub Project (requires `gh-task`):
|
|
|
79
79
|
```bash
|
|
80
80
|
codexapi task -p owner/projects/3 -n "Your Name" -s Ready task_a.yaml task_b.yaml
|
|
81
81
|
```
|
|
82
|
+
Reset owned tasks on a GitHub Project back to Ready:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
codexapi reset -p owner/projects/3
|
|
86
|
+
codexapi reset -p owner/projects/3 -d # also removes the Progress section
|
|
87
|
+
```
|
|
82
88
|
|
|
83
89
|
Task labels are derived from task filenames (basename without extension). The
|
|
84
90
|
issue title/body become `{{item}}` after removing any existing `## Progress`
|
|
@@ -119,12 +125,20 @@ codexapi ralph --cancel --cwd /path/to/project
|
|
|
119
125
|
|
|
120
126
|
Science mode wraps a short task in a science prompt and runs it through the
|
|
121
127
|
Ralph loop. It defaults to `--yolo` and expects progress notes in `SCIENCE.md`.
|
|
128
|
+
Each iteration appends the agent output to `LOGBOOK.md` and the runner extracts
|
|
129
|
+
any improved figures of merit for optional notifications.
|
|
122
130
|
|
|
123
131
|
```bash
|
|
124
132
|
codexapi science "hyper-optimize the kernel cycles"
|
|
125
133
|
codexapi science --no-yolo "hyper-optimize the kernel cycles" --max-iterations 3
|
|
126
134
|
```
|
|
127
135
|
|
|
136
|
+
Optional Pushover notifications: create `~/.pushover` with two non-empty lines.
|
|
137
|
+
Line 1 is your user or group key, line 2 is the app API token. When this file
|
|
138
|
+
exists, Science will send a notification whenever it detects a new best result,
|
|
139
|
+
including the metric values and percent improvement. Task runs will also send a
|
|
140
|
+
✅/❌ notification with the task summary.
|
|
141
|
+
|
|
128
142
|
Run a task file across a list file:
|
|
129
143
|
|
|
130
144
|
```bash
|
|
@@ -2,11 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
from .agent import Agent, agent
|
|
4
4
|
from .foreach import ForeachResult, foreach
|
|
5
|
+
from .pushover import Pushover
|
|
6
|
+
from .ralph import Ralph
|
|
7
|
+
from .science import Science
|
|
5
8
|
from .task import Task, TaskFailed, TaskResult, task, task_result
|
|
6
9
|
|
|
7
10
|
__all__ = [
|
|
8
11
|
"Agent",
|
|
9
12
|
"ForeachResult",
|
|
13
|
+
"Pushover",
|
|
14
|
+
"Ralph",
|
|
15
|
+
"Science",
|
|
10
16
|
"Task",
|
|
11
17
|
"TaskFailed",
|
|
12
18
|
"TaskResult",
|
|
@@ -15,4 +21,4 @@ __all__ = [
|
|
|
15
21
|
"task",
|
|
16
22
|
"task_result",
|
|
17
23
|
]
|
|
18
|
-
__version__ = "0.
|
|
24
|
+
__version__ = "0.6.0"
|
|
@@ -14,7 +14,8 @@ from pathlib import Path
|
|
|
14
14
|
|
|
15
15
|
from .agent import Agent, agent
|
|
16
16
|
from .foreach import foreach
|
|
17
|
-
from .ralph import
|
|
17
|
+
from .ralph import Ralph, cancel_ralph_loop
|
|
18
|
+
from .science import Science
|
|
18
19
|
from .task import DEFAULT_MAX_ITERATIONS, TaskFailed, task
|
|
19
20
|
from .taskfile import TaskFile, load_task_file, task_def_uses_item
|
|
20
21
|
|
|
@@ -26,22 +27,6 @@ _TAIL_MAX_BYTES = 4 * 1024 * 1024
|
|
|
26
27
|
_TAIL_MIN_LINES = 200
|
|
27
28
|
_PROJECT_LOOP_SLEEP = 30
|
|
28
29
|
_ROLL_OUT_PREFIX = "rollout-"
|
|
29
|
-
_SCIENCE_TEMPLATE = (
|
|
30
|
-
"Good afternoon! We have a fun task today - take a good look around this repo "
|
|
31
|
-
"and review all relevant knowledge you have. Our task is to {task}. We're "
|
|
32
|
-
"working step by step in a scientific manner so if there's a SCIENCE.md read "
|
|
33
|
-
"that first to understand the progress of the rest of the team so far. Then "
|
|
34
|
-
"try as hard as you can to find a good path forwards - run as many experiments "
|
|
35
|
-
"as you want and take your time, we have all night. Note down everything you "
|
|
36
|
-
"learn that wasn't obvious in a knowledge section in SCIENCE.md and any "
|
|
37
|
-
"experiments in a similar section. The aim is to move the ball forwards, "
|
|
38
|
-
"either by getting closer to the goal ruling out a hypothesis that doesn't "
|
|
39
|
-
"whilst understanding why. Try your best and have fun with this one! If you "
|
|
40
|
-
"think of several options, pick one and run with it - I will not be available "
|
|
41
|
-
"to make decisions for you, I give you my full permission to explore and make "
|
|
42
|
-
"your own best judgement towards our goal! Remember to update SCIENCE.md. "
|
|
43
|
-
"Good hunting!"
|
|
44
|
-
)
|
|
45
30
|
_TASK_TEMPLATE = (
|
|
46
31
|
"prompt: |\n"
|
|
47
32
|
" Main task prompt. Required. Use {{item}} for per-item values.\n"
|
|
@@ -111,11 +96,6 @@ def _single_line(text):
|
|
|
111
96
|
return " ".join(text.replace("\r", " ").split())
|
|
112
97
|
|
|
113
98
|
|
|
114
|
-
def _science_prompt(task):
|
|
115
|
-
if not isinstance(task, str) or not task.strip():
|
|
116
|
-
raise SystemExit("Science task must be a non-empty string.")
|
|
117
|
-
return _SCIENCE_TEMPLATE.replace("{task}", task.strip())
|
|
118
|
-
|
|
119
99
|
|
|
120
100
|
def _create_task_template(path):
|
|
121
101
|
if not isinstance(path, str) or not path.strip():
|
|
@@ -1496,7 +1476,7 @@ def main(argv=None):
|
|
|
1496
1476
|
if args.command == "ralph":
|
|
1497
1477
|
if args.max_iterations < 0:
|
|
1498
1478
|
raise SystemExit("--max-iterations must be >= 0.")
|
|
1499
|
-
|
|
1479
|
+
Ralph(
|
|
1500
1480
|
prompt,
|
|
1501
1481
|
args.cwd,
|
|
1502
1482
|
args.yolo,
|
|
@@ -1504,21 +1484,20 @@ def main(argv=None):
|
|
|
1504
1484
|
args.max_iterations,
|
|
1505
1485
|
args.completion_promise,
|
|
1506
1486
|
args.ralph_fresh,
|
|
1507
|
-
)
|
|
1487
|
+
)()
|
|
1508
1488
|
return
|
|
1509
1489
|
if args.command == "science":
|
|
1510
1490
|
if args.max_iterations < 0:
|
|
1511
1491
|
raise SystemExit("--max-iterations must be >= 0.")
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
science_prompt,
|
|
1492
|
+
Science(
|
|
1493
|
+
prompt,
|
|
1515
1494
|
args.cwd,
|
|
1516
1495
|
args.yolo,
|
|
1517
1496
|
args.flags,
|
|
1518
1497
|
args.max_iterations,
|
|
1519
1498
|
args.completion_promise,
|
|
1520
1499
|
args.ralph_fresh,
|
|
1521
|
-
)
|
|
1500
|
+
)()
|
|
1522
1501
|
return
|
|
1523
1502
|
if args.command == "task":
|
|
1524
1503
|
if args.project:
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Pushover notification helper."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import threading
|
|
7
|
+
import urllib.error
|
|
8
|
+
import urllib.parse
|
|
9
|
+
import urllib.request
|
|
10
|
+
|
|
11
|
+
_PUSHOVER_PATH = "~/.pushover"
|
|
12
|
+
_PUSHOVER_URL = "https://api.pushover.net/1/messages.json"
|
|
13
|
+
_MAX_MESSAGE = 1024
|
|
14
|
+
|
|
15
|
+
_STARTUP_MESSAGE = (
|
|
16
|
+
"Pushover user and app keys read, notifications for task and science enabled."
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Pushover:
|
|
21
|
+
"""Send Pushover notifications when configured."""
|
|
22
|
+
|
|
23
|
+
_lock = threading.Lock()
|
|
24
|
+
_state = {}
|
|
25
|
+
|
|
26
|
+
def __init__(self, path=_PUSHOVER_PATH):
|
|
27
|
+
self.path = os.path.expanduser(path)
|
|
28
|
+
self._state_ref = self._state_for_path(self.path)
|
|
29
|
+
|
|
30
|
+
def ensure_ready(self, announce=True):
|
|
31
|
+
state = self._state_ref
|
|
32
|
+
with self._lock:
|
|
33
|
+
if not state["checked"]:
|
|
34
|
+
state["checked"] = True
|
|
35
|
+
if not os.path.exists(self.path):
|
|
36
|
+
state["enabled"] = False
|
|
37
|
+
return False
|
|
38
|
+
try:
|
|
39
|
+
tokens = _load_pushover_tokens(self.path)
|
|
40
|
+
except ValueError as exc:
|
|
41
|
+
state["error"] = f"Pushover config error: {exc}"
|
|
42
|
+
raise SystemExit(state["error"]) from None
|
|
43
|
+
state["tokens"] = tokens
|
|
44
|
+
state["enabled"] = True
|
|
45
|
+
if state["error"]:
|
|
46
|
+
raise SystemExit(state["error"]) from None
|
|
47
|
+
if announce and state["enabled"] and not state["announced"]:
|
|
48
|
+
print(_STARTUP_MESSAGE)
|
|
49
|
+
state["announced"] = True
|
|
50
|
+
return state["enabled"]
|
|
51
|
+
|
|
52
|
+
def send(self, title, message):
|
|
53
|
+
tokens = self._get_tokens()
|
|
54
|
+
if not tokens:
|
|
55
|
+
return False
|
|
56
|
+
user_key, app_token = tokens
|
|
57
|
+
title_text = _single_line(title).strip() or "Codex update"
|
|
58
|
+
message_text = (message or "").strip()
|
|
59
|
+
if not message_text:
|
|
60
|
+
return False
|
|
61
|
+
message_text = _truncate(message_text, _MAX_MESSAGE)
|
|
62
|
+
payload = urllib.parse.urlencode(
|
|
63
|
+
{
|
|
64
|
+
"token": app_token,
|
|
65
|
+
"user": user_key,
|
|
66
|
+
"title": title_text,
|
|
67
|
+
"message": message_text,
|
|
68
|
+
}
|
|
69
|
+
).encode("utf-8")
|
|
70
|
+
request = urllib.request.Request(_PUSHOVER_URL, data=payload)
|
|
71
|
+
try:
|
|
72
|
+
with urllib.request.urlopen(request, timeout=10) as response:
|
|
73
|
+
body = response.read().decode("utf-8", errors="replace")
|
|
74
|
+
except urllib.error.HTTPError as exc:
|
|
75
|
+
body = exc.read().decode("utf-8", errors="replace")
|
|
76
|
+
_report_pushover_error(body, exc.code)
|
|
77
|
+
return False
|
|
78
|
+
except Exception as exc:
|
|
79
|
+
_warn(f"Pushover notification failed: {exc}")
|
|
80
|
+
return False
|
|
81
|
+
try:
|
|
82
|
+
data = json.loads(body)
|
|
83
|
+
except json.JSONDecodeError:
|
|
84
|
+
_warn("Pushover returned invalid JSON.")
|
|
85
|
+
return False
|
|
86
|
+
if data.get("status") != 1:
|
|
87
|
+
_report_pushover_error(body, None)
|
|
88
|
+
return False
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
def _get_tokens(self):
|
|
92
|
+
if not self.ensure_ready(announce=False):
|
|
93
|
+
return None
|
|
94
|
+
return self._state_ref["tokens"]
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def _state_for_path(cls, path):
|
|
98
|
+
state = cls._state.get(path)
|
|
99
|
+
if state is None:
|
|
100
|
+
state = {
|
|
101
|
+
"checked": False,
|
|
102
|
+
"enabled": False,
|
|
103
|
+
"tokens": None,
|
|
104
|
+
"error": None,
|
|
105
|
+
"announced": False,
|
|
106
|
+
}
|
|
107
|
+
cls._state[path] = state
|
|
108
|
+
return state
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _load_pushover_tokens(path):
|
|
112
|
+
with open(path, "r", encoding="utf-8") as handle:
|
|
113
|
+
lines = [line.strip() for line in handle if line.strip()]
|
|
114
|
+
if len(lines) != 2:
|
|
115
|
+
raise ValueError(
|
|
116
|
+
f"{path} must contain two non-empty lines: user key then app token"
|
|
117
|
+
)
|
|
118
|
+
return lines[0], lines[1]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _report_pushover_error(body, status_code):
|
|
122
|
+
errors = None
|
|
123
|
+
try:
|
|
124
|
+
data = json.loads(body)
|
|
125
|
+
if isinstance(data, dict):
|
|
126
|
+
errors = data.get("errors")
|
|
127
|
+
except json.JSONDecodeError:
|
|
128
|
+
errors = None
|
|
129
|
+
message = "Pushover notification failed."
|
|
130
|
+
if status_code:
|
|
131
|
+
message = f"{message} HTTP {status_code}."
|
|
132
|
+
detail = _format_pushover_errors(errors)
|
|
133
|
+
if detail:
|
|
134
|
+
message = f"{message} {detail}"
|
|
135
|
+
_warn(message)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _format_pushover_errors(errors):
|
|
139
|
+
if not errors:
|
|
140
|
+
return ""
|
|
141
|
+
if isinstance(errors, str):
|
|
142
|
+
errors = [errors]
|
|
143
|
+
if not isinstance(errors, list):
|
|
144
|
+
return ""
|
|
145
|
+
cleaned = [str(error).strip() for error in errors if str(error).strip()]
|
|
146
|
+
if not cleaned:
|
|
147
|
+
return ""
|
|
148
|
+
hint = []
|
|
149
|
+
lower = " ".join(cleaned).lower()
|
|
150
|
+
if "user" in lower:
|
|
151
|
+
hint.append("Check the user key on line 1 of ~/.pushover.")
|
|
152
|
+
if "token" in lower or "application" in lower:
|
|
153
|
+
hint.append("Check the app token on line 2 of ~/.pushover.")
|
|
154
|
+
if "message" in lower:
|
|
155
|
+
hint.append("Check that the message is not empty or too long.")
|
|
156
|
+
suffix = " ".join(hint)
|
|
157
|
+
if suffix:
|
|
158
|
+
return f"{'; '.join(cleaned)} {suffix}"
|
|
159
|
+
return "; ".join(cleaned)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _single_line(text):
|
|
163
|
+
if not text:
|
|
164
|
+
return ""
|
|
165
|
+
return " ".join(str(text).replace("\r", " ").split())
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _truncate(text, limit):
|
|
169
|
+
if not text:
|
|
170
|
+
return ""
|
|
171
|
+
if len(text) <= limit:
|
|
172
|
+
return text
|
|
173
|
+
if limit <= 3:
|
|
174
|
+
return text[:limit]
|
|
175
|
+
return text[: limit - 3] + "..."
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _warn(message):
|
|
179
|
+
print(message, file=sys.stderr)
|