Spaces:
Sleeping
Sleeping
eas inferenece and added hard, mdeium task
Browse files- inference.py +151 -0
- src/jira_to_code/server/env.py +117 -20
- src/jira_to_code/tasks/easy/calculator.py +3 -0
- src/jira_to_code/tasks/easy/tests/test_calculator.py +6 -0
- src/jira_to_code/tasks/hard/lru_cache.py +13 -0
- src/jira_to_code/tasks/hard/tests/test_cache.py +13 -0
- src/jira_to_code/tasks/medium/formatter.py +7 -0
- src/jira_to_code/tasks/medium/tests/test_formatter.py +9 -0
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=
|
| 18 |
-
file_tree=
|
| 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=
|
| 30 |
-
file_tree=
|
| 31 |
-
current_file_content=
|
| 32 |
-
test_output=
|
| 33 |
-
error=
|
| 34 |
)
|
| 35 |
-
|
| 36 |
-
# Standard format: observation, reward, done, info dict
|
| 37 |
-
return obs, 0.0, False, {}
|
| 38 |
|
| 39 |
async def state(self) -> State:
|
| 40 |
-
""
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 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)"
|