Skip to content

Commit 591125d

Browse files
committed
chore: bump version to 0.1.3 and include default tasks JSON in package
- Updated project version to 0.1.3. - Added `default_tasks.json` to the package, ensuring it is included in both wheel and sdist builds. - Modified task registry to load bundled defaults using `importlib.resources`. - Added tests to verify the inclusion and validity of the bundled tasks.
1 parent 9f9e6d8 commit 591125d

6 files changed

Lines changed: 318 additions & 14 deletions

File tree

pyproject.toml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "shiftgate"
7-
version = "0.1.2"
7+
version = "0.1.3"
88
description = "Intelligent routing layer that automatically selects the right LoRA adapter for each task in your local agent loop."
99
readme = "README.md"
1010
requires-python = ">=3.10"
@@ -50,6 +50,14 @@ Issues = "https://github.com/shiftgate-ai/shiftgate/issues"
5050
[tool.hatch.build.targets.wheel]
5151
packages = ["shiftgate"]
5252

53+
[tool.hatch.build.targets.wheel.force-include]
54+
"shiftgate/data/default_tasks.json" = "shiftgate/data/default_tasks.json"
55+
56+
[tool.hatch.build.targets.sdist]
57+
include = [
58+
"/shiftgate/data/default_tasks.json",
59+
]
60+
5361
[tool.pytest.ini_options]
5462
asyncio_mode = "auto"
5563
testpaths = ["tests"]

shiftgate/data/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Bundled package data for shiftgate."""

shiftgate/data/default_tasks.json

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
[
2+
{
3+
"id": "code_python",
4+
"name": "Python Code Generation",
5+
"description": "Writing, completing, or refactoring Python code including functions, classes, scripts, and modules.",
6+
"validation_examples": [
7+
"Write a Python function to reverse a linked list",
8+
"Create a Python class for a binary search tree",
9+
"Implement quicksort in Python",
10+
"Write a decorator that caches function results in Python",
11+
"Refactor this Python code to use list comprehensions",
12+
"Write a Python script to parse CSV files and compute statistics",
13+
"Create a Python context manager for database connections",
14+
"Generate a Python dataclass for a user profile"
15+
],
16+
"embedding_centroid": null,
17+
"preferred_adapters": [],
18+
"fallback_adapters": []
19+
},
20+
{
21+
"id": "code_sql",
22+
"name": "SQL Query Writing",
23+
"description": "Writing SQL queries including SELECT, JOIN, aggregations, CTEs, window functions, and schema design.",
24+
"validation_examples": [
25+
"Write a SQL query to find duplicate rows in a table",
26+
"Create a SQL query with a LEFT JOIN between orders and customers",
27+
"Write a SQL window function to calculate running totals",
28+
"Generate a SQL CTE to rank employees by department salary",
29+
"Write a SQL query to find users who made purchases in the last 30 days",
30+
"Create a SQL stored procedure for bulk inserts",
31+
"Write a SQL query using GROUP BY and HAVING to filter aggregates",
32+
"Generate a SQL schema for an e-commerce database"
33+
],
34+
"embedding_centroid": null,
35+
"preferred_adapters": [],
36+
"fallback_adapters": []
37+
},
38+
{
39+
"id": "code_javascript",
40+
"name": "JavaScript and TypeScript",
41+
"description": "Writing or reviewing JavaScript and TypeScript code for frontend, Node.js, or full-stack applications.",
42+
"validation_examples": [
43+
"Write a React hook to fetch data from an API",
44+
"Create a TypeScript interface for a REST API response",
45+
"Implement a JavaScript debounce function",
46+
"Write an async function to handle file uploads in Node.js",
47+
"Create a TypeScript generic type for a paginated API response",
48+
"Write a JavaScript event listener for form validation",
49+
"Implement a Promise-based queue in JavaScript",
50+
"Write a Next.js API route that authenticates with JWT"
51+
],
52+
"embedding_centroid": null,
53+
"preferred_adapters": [],
54+
"fallback_adapters": []
55+
},
56+
{
57+
"id": "code_debug",
58+
"name": "Debugging and Error Fixing",
59+
"description": "Diagnosing, explaining, and fixing bugs, exceptions, and unexpected behaviour in any programming language.",
60+
"validation_examples": [
61+
"Fix the KeyError in this Python dictionary access code",
62+
"Why is this JavaScript async function returning undefined?",
63+
"Debug this SQL query that returns no results",
64+
"Find the memory leak in this C++ code",
65+
"Why does this React component re-render infinitely?",
66+
"Fix the NullPointerException in this Java method",
67+
"Explain and fix this Python TypeError: unsupported operand",
68+
"Debug why this API call returns 403 Forbidden",
69+
"Fix the off-by-one error in this loop"
70+
],
71+
"embedding_centroid": null,
72+
"preferred_adapters": [],
73+
"fallback_adapters": []
74+
},
75+
{
76+
"id": "text_summarize",
77+
"name": "Text Summarization",
78+
"description": "Condensing long documents, articles, papers, or conversations into concise summaries.",
79+
"validation_examples": [
80+
"Summarise this research paper in 3 bullet points",
81+
"Give me a TL;DR of this news article",
82+
"Write an executive summary of this annual report",
83+
"Condense this meeting transcript into action items",
84+
"Summarise the key findings from this dataset description",
85+
"Create a one-paragraph abstract for this technical documentation",
86+
"What are the main takeaways from this blog post?",
87+
"Summarise this legal contract in plain English"
88+
],
89+
"embedding_centroid": null,
90+
"preferred_adapters": [],
91+
"fallback_adapters": []
92+
},
93+
{
94+
"id": "text_classify",
95+
"name": "Text Classification and Labeling",
96+
"description": "Classifying, categorising, or labelling text — sentiment, intent, topic, or custom taxonomy.",
97+
"validation_examples": [
98+
"Classify this customer review as positive, negative, or neutral",
99+
"Label these support tickets by urgency level",
100+
"Categorise these product descriptions by department",
101+
"Detect the intent behind this user message: book a flight",
102+
"Tag these news headlines with their topic",
103+
"Classify this email as spam or legitimate",
104+
"Label this text for named entity recognition",
105+
"Determine the sentiment of these tweets",
106+
"Categorise these bug reports by severity"
107+
],
108+
"embedding_centroid": null,
109+
"preferred_adapters": [],
110+
"fallback_adapters": []
111+
},
112+
{
113+
"id": "text_translate",
114+
"name": "Language Translation",
115+
"description": "Translating text between languages, including idioms, technical content, and cultural adaptation.",
116+
"validation_examples": [
117+
"Translate this paragraph from English to French",
118+
"Translate this technical document to Spanish",
119+
"Convert this German medical text to English",
120+
"Translate this Japanese product description to English",
121+
"Localise this marketing copy from English to Portuguese",
122+
"Translate this legal clause from Latin to English",
123+
"Convert this Chinese business email to English",
124+
"Translate this Python error message to plain English"
125+
],
126+
"embedding_centroid": null,
127+
"preferred_adapters": [],
128+
"fallback_adapters": []
129+
},
130+
{
131+
"id": "math_reasoning",
132+
"name": "Math and Quantitative Reasoning",
133+
"description": "Solving arithmetic, algebra, calculus, statistics, logic puzzles, and word problems step by step.",
134+
"validation_examples": [
135+
"Solve this system of linear equations",
136+
"Calculate the derivative of this function",
137+
"What is the probability of drawing two aces from a deck?",
138+
"Solve this word problem: if a train leaves at 3 PM at 60 mph...",
139+
"Compute the integral of x^2 from 0 to 5",
140+
"Find the eigenvalues of this matrix",
141+
"How many ways can 5 people be seated in a row?",
142+
"What is 15% compound interest on $10,000 over 3 years?",
143+
"Prove that the square root of 2 is irrational"
144+
],
145+
"embedding_centroid": null,
146+
"preferred_adapters": [],
147+
"fallback_adapters": []
148+
},
149+
{
150+
"id": "qa_factual",
151+
"name": "Factual Q&A",
152+
"description": "Answering factual questions about history, science, geography, technology, or general knowledge.",
153+
"validation_examples": [
154+
"What is the capital of Australia?",
155+
"Who invented the telephone?",
156+
"What year did World War II end?",
157+
"What is the speed of light in a vacuum?",
158+
"Explain how HTTPS encryption works",
159+
"What is the difference between RAM and ROM?",
160+
"What causes the aurora borealis?",
161+
"How many bones are in the human body?",
162+
"What is the GDP of Germany?"
163+
],
164+
"embedding_centroid": null,
165+
"preferred_adapters": [],
166+
"fallback_adapters": []
167+
},
168+
{
169+
"id": "agent_planning",
170+
"name": "Task Decomposition and Agent Planning",
171+
"description": "Breaking down complex goals into step-by-step plans, sub-tasks, or agent workflows.",
172+
"validation_examples": [
173+
"Create a step-by-step plan to build a REST API from scratch",
174+
"Decompose the task of launching a product into weekly milestones",
175+
"Write a plan for migrating a database to the cloud",
176+
"Break down the goal 'build a recommendation system' into sub-tasks",
177+
"Create an agent workflow to automate data ingestion and reporting",
178+
"Plan the steps to refactor a monolith into microservices",
179+
"What are the subtasks needed to onboard a new engineer?",
180+
"Outline a research plan for evaluating three competing ML models",
181+
"Create a checklist for deploying a web app to production"
182+
],
183+
"embedding_centroid": null,
184+
"preferred_adapters": [],
185+
"fallback_adapters": []
186+
}
187+
]

shiftgate/registry/task_registry.py

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
44
The registry reads from (in priority order):
55
1. ``~/.shiftgate/tasks.json`` — user-edited / previously saved
6-
2. ``<package>/../../data/default_tasks.json`` — bundled defaults
6+
2. ``shiftgate.data/default_tasks.json`` — bundled defaults (via importlib.resources)
77
88
On first run (``shiftgate init``) the ``compute_embeddings`` method is called
99
to populate ``embedding_centroid`` for every cluster and cache them to
@@ -12,6 +12,7 @@
1212

1313
from __future__ import annotations
1414

15+
import importlib.resources
1516
import json
1617
import logging
1718
from pathlib import Path
@@ -31,8 +32,24 @@
3132
_USER_TASKS_PATH = _SHIFTGATE_DIR / "tasks.json"
3233
_CACHE_PATH = _SHIFTGATE_DIR / "embeddings_cache.npy"
3334

34-
# Path to the bundled default tasks, resolved relative to this file's location.
35-
_DEFAULT_TASKS_PATH = Path(__file__).parent.parent.parent / "data" / "default_tasks.json"
35+
# Legacy dev-checkout path kept for backwards compatibility with source trees
36+
# that still ship repo-root ``data/default_tasks.json``.
37+
_LEGACY_DEFAULT_TASKS_PATH = Path(__file__).parent.parent.parent / "data" / "default_tasks.json"
38+
_BUNDLED_DEFAULT_TASKS_LABEL = "shiftgate.data/default_tasks.json"
39+
40+
41+
def _read_bundled_default_tasks() -> str:
42+
"""Return the bundled default task registry JSON from the installed package."""
43+
try:
44+
resource = importlib.resources.files("shiftgate.data") / "default_tasks.json"
45+
return resource.read_text(encoding="utf-8")
46+
except (FileNotFoundError, ModuleNotFoundError, TypeError):
47+
pass
48+
49+
if _LEGACY_DEFAULT_TASKS_PATH.exists():
50+
return _LEGACY_DEFAULT_TASKS_PATH.read_text(encoding="utf-8")
51+
52+
raise FileNotFoundError(_BUNDLED_DEFAULT_TASKS_LABEL)
3653

3754

3855
class TaskRegistry:
@@ -58,22 +75,25 @@ def load(cls) -> "TaskRegistry":
5875
"""Load the task registry from disk.
5976
6077
Prefers the user's ``~/.shiftgate/tasks.json`` and falls back to the
61-
bundled ``data/default_tasks.json`` if the user file does not exist.
78+
bundled ``shiftgate.data/default_tasks.json`` if the user file does not exist.
6279
"""
6380
if _USER_TASKS_PATH.exists():
6481
source = _USER_TASKS_PATH
65-
elif _DEFAULT_TASKS_PATH.exists():
66-
source = _DEFAULT_TASKS_PATH
82+
raw = json.loads(source.read_text(encoding="utf-8"))
6783
else:
68-
raise FileNotFoundError(
69-
f"No task registry found. Expected one of:\n"
70-
f" {_USER_TASKS_PATH}\n"
71-
f" {_DEFAULT_TASKS_PATH}\n"
72-
"Run `shiftgate init` to set up the default registry."
73-
)
84+
try:
85+
raw_text = _read_bundled_default_tasks()
86+
except FileNotFoundError:
87+
raise FileNotFoundError(
88+
f"No task registry found. Expected one of:\n"
89+
f" {_USER_TASKS_PATH}\n"
90+
f" {_BUNDLED_DEFAULT_TASKS_LABEL}\n"
91+
"Run `shiftgate init` to set up the default registry."
92+
) from None
93+
source = Path(_BUNDLED_DEFAULT_TASKS_LABEL)
94+
raw = json.loads(raw_text)
7495

7596
logger.debug("Loading task registry from %s", source)
76-
raw = json.loads(source.read_text(encoding="utf-8"))
7797
tasks = [TaskCluster.model_validate(t) for t in raw]
7898
instance = cls(tasks, source_path=source)
7999

tests/test_packaging.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""
2+
Packaging tests: verify bundled data ships in wheel and sdist builds.
3+
"""
4+
5+
from __future__ import annotations
6+
7+
import json
8+
import subprocess
9+
import tarfile
10+
import zipfile
11+
from pathlib import Path
12+
13+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
14+
15+
16+
def _run_build(*args: str, out_dir: Path) -> None:
17+
subprocess.run(
18+
["uv", "build", *args, "-o", str(out_dir)],
19+
cwd=PROJECT_ROOT,
20+
check=True,
21+
capture_output=True,
22+
text=True,
23+
)
24+
25+
26+
class TestPackaging:
27+
def test_wheel_contains_default_tasks_json(self, tmp_path: Path) -> None:
28+
dist_dir = tmp_path / "dist"
29+
dist_dir.mkdir()
30+
_run_build("--wheel", out_dir=dist_dir)
31+
32+
wheels = list(dist_dir.glob("*.whl"))
33+
assert len(wheels) == 1
34+
35+
with zipfile.ZipFile(wheels[0]) as archive:
36+
names = archive.namelist()
37+
38+
assert "shiftgate/data/default_tasks.json" in names
39+
40+
def test_wheel_default_tasks_json_is_valid(self, tmp_path: Path) -> None:
41+
dist_dir = tmp_path / "dist"
42+
dist_dir.mkdir()
43+
_run_build("--wheel", out_dir=dist_dir)
44+
wheel = next(dist_dir.glob("*.whl"))
45+
46+
with zipfile.ZipFile(wheel) as archive:
47+
raw = archive.read("shiftgate/data/default_tasks.json")
48+
49+
tasks = json.loads(raw)
50+
assert isinstance(tasks, list)
51+
assert len(tasks) == 10
52+
assert tasks[0]["id"]
53+
54+
def test_sdist_contains_default_tasks_json(self, tmp_path: Path) -> None:
55+
dist_dir = tmp_path / "dist"
56+
dist_dir.mkdir()
57+
_run_build("--sdist", out_dir=dist_dir)
58+
59+
sdists = list(dist_dir.glob("*.tar.gz"))
60+
assert len(sdists) == 1
61+
62+
with tarfile.open(sdists[0], "r:gz") as archive:
63+
members = archive.getnames()
64+
65+
assert any(member.endswith("shiftgate/data/default_tasks.json") for member in members)

tests/test_registry.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,29 @@ def test_add_task_overwrites_existing(self, tmp_shiftgate, sample_task):
319319
assert len(reg) == 1
320320
assert reg.get_task("test_task").name == "Updated Task"
321321

322+
def test_load_bundled_defaults_when_no_user_registry(self, tmp_shiftgate):
323+
"""Packaged defaults load via importlib when ~/.shiftgate/tasks.json is absent."""
324+
reg = TaskRegistry.load()
325+
assert len(reg) == 10
326+
assert reg.get_task("code_python") is not None
327+
assert reg.get_task("code_sql") is not None
328+
329+
def test_user_registry_takes_priority_over_bundled_defaults(self, tmp_shiftgate, sample_task):
330+
"""Existing user registries continue to win over bundled defaults."""
331+
import shiftgate.registry.task_registry as tr_mod
332+
333+
user_path = tmp_shiftgate / "tasks.json"
334+
user_path.write_text(
335+
json.dumps([sample_task.model_dump()]),
336+
encoding="utf-8",
337+
)
338+
assert tr_mod._USER_TASKS_PATH == user_path
339+
340+
reg = TaskRegistry.load()
341+
assert len(reg) == 1
342+
assert reg.get_task("test_task") is not None
343+
assert reg.get_task("code_python") is None
344+
322345

323346
# ---------------------------------------------------------------------------
324347
# _auto_link_adapter helper tests

0 commit comments

Comments
 (0)