Navigam commited on
Commit
769cea2
·
1 Parent(s): 31f5053

eas inferenece and added hard, mdeium task

Browse files
inference.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # inference.py
2
+ import asyncio
3
+ import json
4
+ import os
5
+ import textwrap
6
+ from typing import List, Optional
7
+
8
+ from openai import OpenAI
9
+ from dotenv import load_dotenv # <-- Add this
10
+
11
+ # Load environment variables from .env file
12
+ load_dotenv()
13
+
14
+ # Our environment directly for local testing
15
+ from src.jira_to_code.server.env import JiraToCodeEnv
16
+ from src.jira_to_code.models import JiraCodeAction
17
+
18
+ # Read from env (with fallbacks just in case)
19
+ API_BASE_URL = os.getenv("API_BASE_URL", "http://localhost:11434/v1")
20
+ MODEL_NAME = os.getenv("MODEL_NAME", "qwen3.5:2b")
21
+ HF_TOKEN = os.getenv("HF_TOKEN", "dummy")
22
+
23
+ TASK_NAME = "easy-bug-fix"
24
+ BENCHMARK = "jira-to-code"
25
+ MAX_STEPS = 10
26
+ SUCCESS_SCORE_THRESHOLD = 1.0
27
+
28
+ SYSTEM_PROMPT = textwrap.dedent(
29
+ """
30
+ You are an expert software engineer resolving Jira tickets.
31
+ You will receive the objective, the current file tree, and output from actions.
32
+
33
+ You MUST respond with ONLY a valid JSON object representing your next action.
34
+ Do not include markdown blocks, explanations, or any other text.
35
+
36
+ Valid action_types: "read_file", "write_file", "run_tests", "submit"
37
+
38
+ JSON Schema:
39
+ {
40
+ "action_type": "string",
41
+ "file_path": "string or null",
42
+ "content": "string or null"
43
+ }
44
+
45
+ Example 1: {"action_type": "read_file", "file_path": "calculator.py", "content": null}
46
+ Example 2: {"action_type": "write_file", "file_path": "calculator.py", "content": "def add(a, b):\n return a + b"}
47
+ Example 3: {"action_type": "run_tests", "file_path": null, "content": null}
48
+ Example 4: {"action_type": "submit", "file_path": null, "content": null}
49
+ """
50
+ ).strip()
51
+
52
+ # --- MANDATORY LOGGING FUNCTIONS ---
53
+ def log_start(task: str, env: str, model: str) -> None:
54
+ print(f"[START] task={task} env={env} model={model}", flush=True)
55
+
56
+ def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
57
+ error_val = error if error else "null"
58
+ done_val = str(done).lower()
59
+ print(f"[STEP] step={step} action={action} reward={reward:.2f} done={done_val} error={error_val}", flush=True)
60
+
61
+ def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
62
+ rewards_str = ",".join(f"{r:.2f}" for r in rewards)
63
+ print(f"[END] success={str(success).lower()} steps={steps} score={score:.3f} rewards={rewards_str}", flush=True)
64
+
65
+ # --- AGENT LOGIC ---
66
+ def build_user_prompt(step: int, obs, last_reward: float) -> str:
67
+ return textwrap.dedent(
68
+ f"""
69
+ Step: {step}
70
+ Ticket: {obs.jira_ticket}
71
+ File Tree: {obs.file_tree}
72
+ Last Read/Write Content: {obs.current_file_content or 'None'}
73
+ Test Output: {obs.test_output or 'None'}
74
+ Error: {obs.error or 'None'}
75
+ Last Reward: {last_reward}
76
+
77
+ Determine your next action and return ONLY the JSON.
78
+ """
79
+ ).strip()
80
+
81
+ def get_action_from_llm(client: OpenAI, step: int, obs, last_reward: float) -> JiraCodeAction:
82
+ user_prompt = build_user_prompt(step, obs, last_reward)
83
+ try:
84
+ completion = client.chat.completions.create(
85
+ model=MODEL_NAME,
86
+ messages=[
87
+ {"role": "system", "content": SYSTEM_PROMPT},
88
+ {"role": "user", "content": user_prompt},
89
+ ],
90
+ temperature=0.1, # Keep it low for coding/JSON accuracy
91
+ max_tokens=500,
92
+ )
93
+ raw_text = (completion.choices[0].message.content or "").strip()
94
+
95
+ # Clean up in case the model wraps JSON in markdown
96
+ if raw_text.startswith("```json"):
97
+ raw_text = raw_text.replace("```json", "", 1).replace("```", "", 1).strip()
98
+
99
+ action_dict = json.loads(raw_text)
100
+ return JiraCodeAction(**action_dict), raw_text
101
+
102
+ except Exception as exc:
103
+ # Fallback action if parsing fails so the environment doesn't crash
104
+ error_msg = f"LLM Parsing Error: {exc}"
105
+ return JiraCodeAction(action_type="read_file", file_path="calculator.py"), error_msg
106
+
107
+ # --- MAIN LOOP ---
108
+ async def main() -> None:
109
+ client = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN)
110
+ env = JiraToCodeEnv()
111
+
112
+ rewards: List[float] = []
113
+ steps_taken = 0
114
+ score = 0.0
115
+ success = False
116
+
117
+ log_start(task=TASK_NAME, env=BENCHMARK, model=MODEL_NAME)
118
+
119
+ try:
120
+ obs = await env.reset()
121
+ last_reward = 0.0
122
+
123
+ for step in range(1, MAX_STEPS + 1):
124
+ action, raw_action_str = get_action_from_llm(client, step, obs, last_reward)
125
+
126
+ # Escape newlines for single-line logging
127
+ safe_action_str = raw_action_str.replace('\n', '\\n').replace('\r', '')
128
+
129
+ # Take step in environment
130
+ obs, reward, done, _ = await env.step(action)
131
+ error = obs.error
132
+
133
+ rewards.append(reward)
134
+ steps_taken = step
135
+ last_reward = reward
136
+
137
+ log_step(step=step, action=safe_action_str, reward=reward, done=done, error=error)
138
+
139
+ if done:
140
+ break
141
+
142
+ # Calculate final score (clamp between 0 and 1)
143
+ score = min(max(sum(rewards), 0.0), 1.0)
144
+ success = score >= SUCCESS_SCORE_THRESHOLD
145
+
146
+ finally:
147
+ await env.close()
148
+ log_end(success=success, steps=steps_taken, score=score, rewards=rewards)
149
+
150
+ if __name__ == "__main__":
151
+ asyncio.run(main())
src/jira_to_code/server/env.py CHANGED
@@ -1,41 +1,138 @@
1
  # src/jira_to_code/server/env.py
 
 
 
 
 
2
  from typing import Tuple, Dict, Any
 
3
  from openenv.core.env_server import Environment, State
4
  from src.jira_to_code.models import JiraCodeAction, JiraCodeObservation
5
 
6
  class JiraToCodeEnv(Environment):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  def __init__(self):
8
  super().__init__()
9
- self.current_task = None
10
- self.workspace_dir = None
11
  self.step_count = 0
 
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  async def reset(self) -> JiraCodeObservation:
14
- """Initialize a new episode, return initial observation."""
15
  self.step_count = 0
 
 
 
 
 
 
 
 
 
 
16
  return JiraCodeObservation(
17
- jira_ticket="TICKET-123: Fix off-by-one error in calculator.py",
18
- file_tree=["calculator.py", "tests/test_calculator.py"],
19
- current_file_content=None,
20
- test_output=None,
21
- error=None
22
  )
23
 
24
  async def step(self, action: JiraCodeAction) -> Tuple[JiraCodeObservation, float, bool, Dict[str, Any]]:
25
- """Execute action, return resulting Observation, reward, done, info."""
26
  self.step_count += 1
27
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  obs = JiraCodeObservation(
29
- jira_ticket="TICKET-123: Fix off-by-one error in calculator.py",
30
- file_tree=["calculator.py", "tests/test_calculator.py"],
31
- current_file_content="def add(a, b):\n return a + b + 1 # BUG",
32
- test_output=None,
33
- error=None
34
  )
35
-
36
- # Standard format: observation, reward, done, info dict
37
- return obs, 0.0, False, {}
38
 
39
  async def state(self) -> State:
40
- """Access episode metadata."""
41
- return State(episode_id="test-123", step_count=self.step_count)
 
 
 
 
1
  # src/jira_to_code/server/env.py
2
+ import os
3
+ import shutil
4
+ import tempfile
5
+ import subprocess
6
+ from pathlib import Path
7
  from typing import Tuple, Dict, Any
8
+
9
  from openenv.core.env_server import Environment, State
10
  from src.jira_to_code.models import JiraCodeAction, JiraCodeObservation
11
 
12
  class JiraToCodeEnv(Environment):
13
+ TASKS = {
14
+ "easy": {
15
+ "dir": "src/jira_to_code/tasks/easy",
16
+ "ticket": "TICKET-101: Fix the off-by-one bug in calculator.add() function. It should correctly sum two numbers."
17
+ },
18
+ "medium": {
19
+ "dir": "src/jira_to_code/tasks/medium",
20
+ "ticket": "TICKET-201: Implement format_user_data in formatter.py. It should format dictionary data to 'LAST_NAME, First_name (Age: X)'. Handle missing age by defaulting to 'Unknown'."
21
+ },
22
+ "hard": {
23
+ "dir": "src/jira_to_code/tasks/hard",
24
+ "ticket": "TICKET-301: Implement an LRUCache class in lru_cache.py with put() and get() methods. O(1) time complexity expected. Evict least recently used when capacity is reached."
25
+ }
26
+ }
27
+
28
  def __init__(self):
29
  super().__init__()
 
 
30
  self.step_count = 0
31
+ self.workspace_dir = None
32
 
33
+ # Determine which task to run based on environment variable (defaults to easy)
34
+ self.task_level = os.getenv("JIRA_TASK_LEVEL", "easy").lower()
35
+ if self.task_level not in self.TASKS:
36
+ self.task_level = "easy"
37
+
38
+ self.task_source_dir = Path(self.TASKS[self.task_level]["dir"]).resolve()
39
+ self.jira_ticket = self.TASKS[self.task_level]["ticket"]
40
+
41
+ def _get_file_tree(self) -> list[str]:
42
+ if not self.workspace_dir:
43
+ return []
44
+ tree = []
45
+ for root, _, files in os.walk(self.workspace_dir):
46
+ for file in files:
47
+ if "__pycache__" in root or file.endswith(".pyc"):
48
+ continue
49
+ rel_path = Path(root) / file
50
+ tree.append(str(rel_path.relative_to(self.workspace_dir)))
51
+ return tree
52
+
53
  async def reset(self) -> JiraCodeObservation:
 
54
  self.step_count = 0
55
+ if self.workspace_dir and Path(self.workspace_dir).exists():
56
+ shutil.rmtree(self.workspace_dir)
57
+
58
+ self.workspace_dir = tempfile.mkdtemp(prefix=f"jira_env_{self.task_level}_")
59
+
60
+ if self.task_source_dir.exists():
61
+ shutil.copytree(self.task_source_dir, self.workspace_dir, dirs_exist_ok=True)
62
+ else:
63
+ print(f"Warning: Task directory {self.task_source_dir} not found!")
64
+
65
  return JiraCodeObservation(
66
+ jira_ticket=self.jira_ticket,
67
+ file_tree=self._get_file_tree()
 
 
 
68
  )
69
 
70
  async def step(self, action: JiraCodeAction) -> Tuple[JiraCodeObservation, float, bool, Dict[str, Any]]:
 
71
  self.step_count += 1
72
+ reward = 0.0
73
+ done = False
74
+ current_file_content = None
75
+ test_output = None
76
+ error = None
77
+
78
+ workspace_path = Path(self.workspace_dir).resolve()
79
+
80
+ try:
81
+ if action.action_type in ["read_file", "write_file"]:
82
+ if not action.file_path:
83
+ error = "file_path must be provided for read/write actions."
84
+ else:
85
+ target_path = (workspace_path / action.file_path).resolve()
86
+ if not target_path.is_relative_to(workspace_path):
87
+ error = "Access denied: cannot access files outside workspace."
88
+ elif action.action_type == "read_file":
89
+ if target_path.exists():
90
+ current_file_content = target_path.read_text()
91
+ else:
92
+ error = f"File not found: {action.file_path}"
93
+ elif action.action_type == "write_file":
94
+ if action.content is None:
95
+ error = "content must be provided for write_file action."
96
+ else:
97
+ target_path.parent.mkdir(parents=True, exist_ok=True)
98
+ target_path.write_text(action.content)
99
+ current_file_content = action.content
100
+
101
+ elif action.action_type == "run_tests":
102
+ result = subprocess.run(["pytest"], cwd=self.workspace_dir, capture_output=True, text=True)
103
+ test_output = result.stdout + "\n" + result.stderr
104
+ if result.returncode == 0:
105
+ reward = 0.5
106
+ elif result.returncode == 1:
107
+ reward = 0.1
108
+ else:
109
+ reward = -0.1
110
+
111
+ elif action.action_type == "submit":
112
+ result = subprocess.run(["pytest"], cwd=self.workspace_dir, capture_output=True, text=True)
113
+ test_output = result.stdout + "\n" + result.stderr
114
+ done = True
115
+ if result.returncode == 0:
116
+ reward = 1.0
117
+ else:
118
+ reward = 0.0
119
+
120
+ except Exception as e:
121
+ error = f"System error: {str(e)}"
122
+ reward = -0.2
123
+
124
  obs = JiraCodeObservation(
125
+ jira_ticket=self.jira_ticket,
126
+ file_tree=self._get_file_tree(),
127
+ current_file_content=current_file_content,
128
+ test_output=test_output,
129
+ error=error
130
  )
131
+ return obs, reward, done, {}
 
 
132
 
133
  async def state(self) -> State:
134
+ return State(episode_id=f"jira-{self.task_level}-{self.step_count}", step_count=self.step_count)
135
+
136
+ async def close(self):
137
+ if self.workspace_dir and Path(self.workspace_dir).exists():
138
+ shutil.rmtree(self.workspace_dir)
src/jira_to_code/tasks/easy/calculator.py CHANGED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ def add(a, b):
2
+ # BUG: Off by one error
3
+ return a + b + 1
src/jira_to_code/tasks/easy/tests/test_calculator.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from calculator import add
2
+
3
+ def test_add():
4
+ assert add(2, 3) == 5
5
+ assert add(0, 0) == 0
6
+ assert add(-1, 1) == 0
src/jira_to_code/tasks/hard/lru_cache.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class LRUCache:
2
+ def __init__(self, capacity: int):
3
+ # TODO: Initialize your cache data structures here
4
+ pass
5
+
6
+ def get(self, key: int) -> int:
7
+ # TODO: Return the value if key exists, otherwise return -1.
8
+ # Accessing an item makes it the most recently used.
9
+ pass
10
+
11
+ def put(self, key: int, value: int) -> None:
12
+ # TODO: Add key-value pair. If capacity is exceeded, evict the least recently used item.
13
+ pass
src/jira_to_code/tasks/hard/tests/test_cache.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from lru_cache import LRUCache
2
+
3
+ def test_lru_cache_operations():
4
+ cache = LRUCache(2)
5
+ cache.put(1, 1)
6
+ cache.put(2, 2)
7
+ assert cache.get(1) == 1 # returns 1
8
+ cache.put(3, 3) # evicts key 2
9
+ assert cache.get(2) == -1 # returns -1 (not found)
10
+ cache.put(4, 4) # evicts key 1
11
+ assert cache.get(1) == -1 # returns -1 (not found)
12
+ assert cache.get(3) == 3 # returns 3
13
+ assert cache.get(4) == 4 # returns 4
src/jira_to_code/tasks/medium/formatter.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ def format_user_data(user_dict):
2
+ """
3
+ Takes a dictionary with 'first_name', 'last_name', and 'age'.
4
+ Should return a string formatted exactly like: "LAST_NAME, First_name (Age: X)"
5
+ If 'age' is missing, it should default to "Unknown".
6
+ """
7
+ pass # TODO: Implement this function based on the docstring
src/jira_to_code/tasks/medium/tests/test_formatter.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from formatter import format_user_data
2
+
3
+ def test_format_full_data():
4
+ data = {"first_name": "john", "last_name": "doe", "age": 30}
5
+ assert format_user_data(data) == "DOE, John (Age: 30)"
6
+
7
+ def test_format_missing_age():
8
+ data = {"first_name": "alice", "last_name": "smith"}
9
+ assert format_user_data(data) == "SMITH, Alice (Age: Unknown)"