Skip to content

Commit c8214bd

Browse files
fix: job_chat drops generated code when the job is empty (#540)
* fix(job_chat): write code into empty jobs instead of dropping it (#539) When a job body was empty, generate() skipped applying the model's edit_job output because it was gated on `if job_code:`. The assistant returned only a preamble ("I'll add a fn()...") with no code, since there was nothing to patch. Fix: - generate(): apply edits even when the job is empty (blank-file case). - apply_single_edit(): when the current code is empty, the edit's new_code is the result regardless of action, so correctness no longer depends on the model choosing "rewrite" and we avoid a wasted error-correction round-trip. - prompt: nudge the model toward "rewrite" on empty jobs. - drop the matching empty-original short-circuit in parse_and_apply_edits. Adds unit tests covering rewrite, replace, whitespace-only, and no-edit cases. * changeset * version: 1.3.3 --------- Co-authored-by: Joe Clark <jclark@openfn.org>
1 parent e70c704 commit c8214bd

6 files changed

Lines changed: 120 additions & 16 deletions

File tree

.changeset/ripe-poets-enjoy.md

Lines changed: 0 additions & 5 deletions
This file was deleted.

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# apollo
22

3+
## 1.3.3
4+
5+
### Patch Changes
6+
7+
- 824b99f: Fix an issue in job_chat where no code is returned if an an empty job
8+
expression is sent
9+
- e5daf45: update dependencies
10+
311
## 1.3.2
412

513
### Patch Changes

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "apollo",
33
"module": "platform/index.ts",
4-
"version": "1.3.2",
4+
"version": "1.3.3",
55
"type": "module",
66
"scripts": {
77
"start": "NODE_ENV=production bun platform/src/index.ts",

services/job_chat/job_chat.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -315,12 +315,13 @@ def generate(
315315

316316
if suggest_code is True and tool_code_edits:
317317
job_code = context.get("expression") if isinstance(context, dict) else None
318-
if job_code:
319-
with sentry_sdk.start_span(description="apply_code_edits"):
320-
suggested_code, diff = self.apply_code_edits(
321-
content=content, text_answer=text_response,
322-
original_code=job_code, code_edits=tool_code_edits,
323-
)
318+
# Apply edits even for an empty job: it's just the blank-file
319+
# case. See https://github.com/OpenFn/apollo/issues/539.
320+
with sentry_sdk.start_span(description="apply_code_edits"):
321+
suggested_code, diff = self.apply_code_edits(
322+
content=content, text_answer=text_response,
323+
original_code=job_code or "", code_edits=tool_code_edits,
324+
)
324325
# If the model called the tool but emitted no prose, give the user
325326
# a short confirmation so the response isn't empty.
326327
if not text_response and suggested_code:
@@ -421,10 +422,10 @@ def parse_and_apply_edits(self, response: str, content: str, original_code: Opti
421422
text_answer = response_data.get("text_answer", "").strip()
422423
code_edits = response_data.get("code_edits", [])
423424

424-
if not code_edits or not original_code:
425+
if not code_edits:
425426
return text_answer, None, None
426-
427-
suggested_code, diff = self.apply_code_edits(content=content, text_answer=text_answer, original_code=original_code, code_edits=code_edits)
427+
428+
suggested_code, diff = self.apply_code_edits(content=content, text_answer=text_answer, original_code=original_code or "", code_edits=code_edits)
428429
return text_answer, suggested_code, diff
429430

430431
except json.JSONDecodeError as e:
@@ -472,7 +473,16 @@ def apply_single_edit(self, content: str, text_answer: str, code: str, edit: Dic
472473
})
473474

474475
action = edit.get("action")
475-
476+
477+
# Empty job: there is nothing to edit against, so the new code IS the
478+
# result regardless of action (replace vs rewrite). Handling this here
479+
# keeps correctness independent of which action the model picks.
480+
# See https://github.com/OpenFn/apollo/issues/539.
481+
if not code.strip():
482+
new_code = edit.get("new_code")
483+
if new_code:
484+
return new_code, True, None
485+
476486
if action == "replace":
477487
old_code = edit.get("old_code")
478488
new_code = edit.get("new_code")

services/job_chat/prompt.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,10 @@
184184
- {"action": "replace", "old_code": "<exact code to find>", "new_code": "<replacement>"}
185185
- {"action": "rewrite", "new_code": "<complete new code>"}
186186
187+
If the current job is EMPTY (no code at all), you MUST use the "rewrite" action
188+
with the complete code. There is nothing to "replace" in an empty job, so a
189+
"replace" edit will not apply.
190+
187191
<code editing rules>
188192
- old_code must match the user's code EXACTLY, including all whitespace and indentation.
189193
- Edits apply sequentially — later edits work on the already-modified code.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""Unit tests for writing code into an empty/new job (issue #539).
2+
3+
Before the fix, the assistant silently dropped generated code when the job body
4+
was empty: `generate()` only applied edits `if job_code:`, and
5+
`parse_and_apply_edits()` short-circuited on `not original_code`. The user saw a
6+
preamble ("I'll add a fn()...") with no code.
7+
8+
These exercise the pure edit-application methods. We build the instance with
9+
`__new__` to skip `__init__` (which constructs an Anthropic client, blocked in
10+
unit tests); the `rewrite` path never touches the LLM client.
11+
"""
12+
13+
import json
14+
15+
from job_chat.job_chat import AnthropicClient
16+
17+
18+
def _client():
19+
return AnthropicClient.__new__(AnthropicClient)
20+
21+
22+
def test_rewrite_into_empty_job_returns_full_code():
23+
suggested, diff = _client().apply_code_edits(
24+
content="write a simple http post",
25+
text_answer="I'll add it",
26+
original_code="",
27+
code_edits=[{"action": "rewrite", "new_code": "post('https://x.test', state.data);"}],
28+
)
29+
30+
assert suggested == "post('https://x.test', state.data);"
31+
assert diff["patches_applied"] == 1
32+
33+
34+
def test_replace_action_into_empty_job_writes_code():
35+
# The model may send a "replace" against an empty job. With nothing to find,
36+
# the new code is still the result, deterministically and without an LLM
37+
# error-correction round-trip.
38+
suggested, diff = _client().apply_code_edits(
39+
content="write a simple http post",
40+
text_answer="I'll add it",
41+
original_code="",
42+
code_edits=[
43+
{"action": "replace", "old_code": "// anything", "new_code": "post('https://x.test');"}
44+
],
45+
)
46+
47+
assert suggested == "post('https://x.test');"
48+
assert diff["patches_applied"] == 1
49+
50+
51+
def test_whitespace_only_job_treated_as_empty():
52+
suggested, diff = _client().apply_code_edits(
53+
content="write a simple http post",
54+
text_answer="I'll add it",
55+
original_code=" \n\n ",
56+
code_edits=[{"action": "rewrite", "new_code": "post('https://x.test');"}],
57+
)
58+
59+
assert suggested == "post('https://x.test');"
60+
assert diff["patches_applied"] == 1
61+
62+
63+
def test_parse_and_apply_no_longer_drops_code_on_empty_original():
64+
response = json.dumps(
65+
{
66+
"text_answer": "I'll write it",
67+
"code_edits": [{"action": "rewrite", "new_code": "get('https://x.test');"}],
68+
}
69+
)
70+
71+
text, suggested, diff = _client().parse_and_apply_edits(
72+
response, content="x", original_code=""
73+
)
74+
75+
assert suggested == "get('https://x.test');"
76+
assert diff["patches_applied"] == 1
77+
78+
79+
def test_no_code_edits_still_returns_none():
80+
response = json.dumps({"text_answer": "just explaining", "code_edits": []})
81+
82+
text, suggested, diff = _client().parse_and_apply_edits(
83+
response, content="x", original_code=""
84+
)
85+
86+
assert suggested is None
87+
assert diff is None

0 commit comments

Comments
 (0)